문서의 임의 삭제는 제재 대상으로, 문서를 삭제하려면 삭제 토론을 진행해야 합니다. 문서 보기문서 삭제토론 C++ (문단 편집) ==== 메타 프로그래밍 ==== >TMP ('''T'''emplate '''M'''eta '''P'''rogramming). 템플릿을 사용한 메타 프로그래밍 [[https://en.wikipedia.org/wiki/Template_metaprogramming|Wikipedia]] C++에서는 템플릿을 사용해 메타 프로그래밍을 할 수 있다. 여기서 [[메타]]란 [[메타데이터|Meta Data (서술 자료)]]를 의미한다. 값 뿐만 아니라 프로그램, 바이너리, 속성 그 자체도 데이터로 취급한다. 가령 [[Python]]의 메타 클래스는 '클래스'를 생성하며 [* '클래스의 인스턴스'가 아니다.] 클래스의 바이트 크기, 필드, 메서드 등을 정의한다. 또한 [[Python]], [[C\#]], [[Swift]] 등 많은 언어가 {{{int}}}, {{{double}}} 같은 원시 자료형도 클래스로 보여준다. 하지만 C++은 그럴 수가 없다. C++의 클래스는 메모리 덩어리고, {{{int}}}라는 단어 자체에는 그저 컴파일러가 {{{int}}}를 메타 정보로 가진 객체는 32비트 크기로 읽겠다, 말고는 아무 의미도 없다. 다른 언어는 '정보[* 변수]의 메타 정보[* 자료형]의 메타 정보[* 메모리 정렬 방식, 사용자가 접근할 수 있는 메모리 영역, 바이트 크기, '''메서드의 이름과 코드''', '''필드의 이름과 정보''']를 언어 자체적으로 가져오고, 심지어 정의할 수도 있다. [[C\#]], [[Javascript]], [[Python]]이 대표적인 예시다. 반면 C++에선 클래스가 그냥 메모리 덩어리고 메서드도 그냥 쓰기 좋게 만든 함수 포인터다. 전처리기, 컴파일러 확장, 미리 쓰여진 코드에 의존할 뿐 자료형 자체에 대해서는 아무것도 알 수 없다. {{{sizeof}}}, {{{alignof}}}, {{{alignas}}} 키워드가 있지만 부족하다. 그리고 직접 클래스 자체의 속성을 정의하려면 {{{using}}}, {{{static}}} 변수를 사용해 일일히 모든 클래스에 메타 정보를 기입해야 한다. 이 방법은 임의의 클래스, 그리고 원시 자료형에는 적용할 수 없는 문제가 있다. 이 간극을 메우기 위해 대신 템플릿을 사용해 메타 프로그래밍을 시도하는 것이다. 같은 템플릿 문법을 사용하지만 앞 문단의 일반화 프로그래밍과는 엄연히 목표가 다르다. 일반화 프로그래밍이 자료형을 드러내어 컴파일 시기에든 실행 시기에든 코드 확장 및 범용성 증대를 목표로 한다면, 메타 프로그래밍은 명시해야할 자료형을 숨기고, 어떤 자료형에 대해 그 자료형에 대한 정보를 기술하는 것을 목표로 한다. 이는 일반화 프로그래밍과는 다르게 확장이 아니고 특정 자료형에만 코드를 생성하도록 만든다. * SFINAE('''S'''ubstitution '''F'''ailure '''I'''s '''N'''ot '''A'''n '''E'''rror): 함수를 오버로딩 하는데 있어 조건에 따라 일부러 오류가 발생하는 템플릿의 구현 코드를 발생시켜 틀리지 않은 특정 구현만 선택되게 만드는 테크닉이다. C++의 템플릿은 자료형의 상태를 서술하기 위해 분명히 존재하지만, 실제로 인스턴스화되는 시점은 런타임 직전 사용자의 코드에서 실제 자료형을 가져와야 한다. C++ 문법의 틈새에 존재하는 문법 오류이지만 런타임 오류가 아닌 상황을 적극 활용하는 것이다. 직역하면 '대입 실패는 오류가 아니다'라는 뜻이다. ---- 그렇다면 C++에서 그냥 메타 프로그래밍이라고 안하고 굳이 앞에 템플릿이라고 붙인 이유가 있을까? C++의 템플릿은 독특한 특성을 많이 가지고 있다. 템플릿 매개변수는 반드시 컴파일 시점에 결정된다. 템플릿 매개변수는 자료형 뿐만 아니라 값도 전달할 수 있다. 템플릿의 실제 코드는 늦게 평가된다. 템플릿은 분명 존재하는 C++ 코드이지만, 실질적으로 전처리기 구문과 다름이 없다. 실행 시점에는 이미 바이너리로 모든 경우에 대해 완성된 상태가 되므로 컴파일 이후에 실행 시간에는 아무 영향이 없다 [* 이를 '결정적'이라고 한다.]. 컴파일러가 예측할 수 없는[* 이를 '비결정적'이라고 한다.] 동적 메모리 할당, 메모리 해제, 네트워크 단의 작업, 그리고 운영제체 호출을 제외하면 모두 상수 시간에 결정할 수 있다. 곧 C++의 메타 프로그래밍은 자료형의 상태를 기술하는 것에 더해서, 할 수 있는 작업은 컴파일러를 고문해 모조리 미리 처리하는 것이 지상과제가 된다. 이는 C++의 템플릿 문법이 컴파일 시간에 [[튜링완전|Turing complete]]하기 때문에 이런 일이 가능한 것이다. C++ 안에 컴파일러 전용의 또 다른 언어가 숨어있는 것과 같은 상황이다. 다른 언어에서는 비슷한 것도 찾기 힘들다. * Expression Templates: 디자인 패턴의 일종인 Proxy pattern 기반의 Lazy evaluation이 적용되는 효율적인 계산 코드를 컴파일 시점에 생성하는 기법이다. 일반적으로 연산 도중의 임시 객체 생성 문제를 이 기법을 통해 해결하는 경우가 있다. RVO(Return Value Optimization)를 감안하더라도 C++ 특성상 연산자를 활용하는 과정에서, 직접적인 연산을 시도하면 임시 객체의 생성을 완전히 막을 수는 없기 때문이다. 배우기 어렵고 알아보기도 힘들고, 실행 성능을 포기하지 않는 대신 컴파일 시간을 '''심각하게''' 포기했다. 심지어 디버깅도 힘들다. 다만 디버그 문제는 자기가 원하는 템플릿에 맞춰 중단점을 거는 게 가능해지는 등 많이 개선된 편이다. 당연히 [[템플릿]]에 대하여 깊은 이해가 없다면 아예 이해할 수가 없는 개념이기도 하다. 그래도 알아두면 은근 써먹을 데가 많다. C++에서는 단순히 특정 자료형의 속성 선언부터 함자 클래스[* '''Functor, Niebloid, Function Object'''. () 연산자 오버로딩을 통해 함수 인터페이스를 구현한다]를 이용한 방문자 패턴, 트레잇, 타입 리스트, CRTP, mixin 등 수많은 고성능 디자인 패턴은 C++에서는 TMP의 도움 없이는 시도조차 할 수 없다. C++17 기준으로 쓰여진 [[https://www.amazon.com/C-Templates-Complete-Guide-2nd/dp/0321714121|C++ 템플릿 기본서]] 는 그 쪽수가 800쪽을 넘는다. 예를 들어, [[피보나치 수열]]을 계산하는 코드는 다음과 같이 쓸 수 있다. {{{#!syntax cpp constexpr size_t fibonacci(size_t n) noexcept { return (n < 2) ? n : fibonacci(n - 1) + fibonacci(n - 2); } constexpr size_t result = fibonacci(7); // 13 }}} 아래는 템플릿을 활용해 컴파일 시점에 계산하는 코드이다. [* GCC와 같은 컴파일러는 최적화 옵션을 넣지 않을 경우 {{{constexpr}}} 함수를 컴파일 시점에 계산하지 않는데, 아래와 같이 템플릿을 섞어서 사용하면 확실히 컴파일 시점에 계산된다.] {{{#!folding 템플릿 코드 {{{#!syntax cpp template struct Fibonacci { static constexpr size_t Value = Fibonacci::Value + Fibonacci::Value; }; template<> struct Fibonacci<0> { static constexpr size_t Value = 0; }; template<> struct Fibonacci<1> { static constexpr size_t Value = 1; }; int arr[Fibonacci<7>::Value]; }}} C++17의 {{{if constepxr}}} 또는 C++20의 {{{consteval}}}을 활용하면 더 간결하게 쓸 수 있다. {{{#!syntax cpp template constexpr size_t Fibonacci() noexcept // C++20 이후에는 consteval로 지정해도 문제없다. { if constexpr (N < 2) return N; else return Fibonacci() + Fibonacci(); } int arr[Fibonacci<7>()]; }}} }}} 실무에서 가장 많이 쓰이는 테크닉 중 하나로 CRTP (Curiously Recursive Template Pattern) 패턴이 있다. 추상 클래스를 구현할 시 가상 함수 호출에 따른 오버헤드를 막고자 고안되었다. 이는 다음과 같이 쓴다. {{{#!folding CRTP 예제 코드 {{{#!syntax cpp template class Base { constexpr void Function() noexcept(noexcept(Cast()->Function())) { Cast()->Function(); } protected: constexpr Derived* Cast() noexcept { return static_cast(this); } constexpr const Derived* Cast() const noexcept { return static_cast(this); } }; class CRTPDerived1 : public Base { public: void Function() noexcept // Base::Function은 상수식이 아니고 noexcept(true) { std::print("CRTPDerived1"); } }; template class CRTPDerived2 : public Base> { public: constexpr void Function() // Base::Function은 상수식이지만 noexcept(false) { throw "CRTPDerived2"; } }; }}} }}} 파생 클래스를 템플릿 인자로 기반 클래스에 넘겨줌으로써, 기반 클래스는 멤버 함수의 동작을 각 파생 클래스에 대해 특수화할 수 있다. '''템플릿 인자는 컴파일 타임에 알 수 있는 내용이므로, 프로그램 내에서 기반 클래스가 어떤 파생 클래스로 인스턴스화되는지 컴파일 타임에 알 수 있다.''' 따라서 런타임 가상 함수 디스패치 및 그에 따른 오버헤드가 필요없다! 이는 멤버 함수 호출이 잦은데 상속은 구현하고 싶은 경우에 성능상으로 엄청난 차이를 낸다.저장 버튼을 클릭하면 당신이 기여한 내용을 CC-BY-NC-SA 2.0 KR으로 배포하고,기여한 문서에 대한 하이퍼링크나 URL을 이용하여 저작자 표시를 하는 것으로 충분하다는 데 동의하는 것입니다.이 동의는 철회할 수 없습니다.캡챠저장미리보기