C++공부/C++ Templates

8. 컴파일 과정 프로그래밍

아헿헿헿 2022. 6. 11. 06:50

C++98에서부터 템플릿은 컴팡리 과정에 계산하는 기능을 제공하여, 루프를 사용하거나 실행 경로를 선택하는 것이 가능해졌습니다. C++11과 C++14에서 컴팡리 과정 계산이 훨씬 좋아지며, 직관적인 실행 경로 선택과 대부분의 명령문을 사용하는 constexpr 특성이 추가되었습니다. C++17에서는 컴파일 과정 if를 추가하여 조건이나 제약 사항에 따라 명령문을 버릴 수 있게 되었습니다.

8.1 템플릿 메타프로그래밍

템플릿은 컴파일 과정에 인스턴스화되며, C++ 템플릿의 특성 몇가지를 인스턴스 과정과 결합시키면 C++ 언어 자체에서 기초적인 재귀 프로그래밍 언어를 생성할 수 있습니다. 밑의 예시는 팩토리얼을 재귀로 구현한 내용입니다. 자세한 것은 23장을 참조하면 됩니다.

template<unsigned long long N>
struct factorial{
    static const unsigned long long value = N * factorial<N - 1>::value;
};
template<>
struct factorial<1LL>{
    static const unsigned long long value = 1;
};

8.2 constexpr로 계산

C++11에서 도입되어 C++14에서는 제약 사항의 대부분이 제거 됐는데, 이를 성공적으로 평가하려면 모든 계산 과정이 컴파일하는 동안 가능해야하며 유효해야만 합니다. 현재는 힙 할당이나 예외 던지기 같은 작업은 지원되지 않습니다. C++11에서는 한개의 명령문만 있어야 하기에 삼항 연산자만을 사용하고, 반복할 떄는 재귀를 사용하지만  C++14부터는 이러한 제어 구조를 사용 가능하기에 for문이 활용가능합니다.

constexpr bool isPrime (unsigned int p)
{
  for (unsigned int d=2; d<=p/2; ++d) {
    if (p % d == 0) {
      return false;  // found divisor without remainder
    }
  }
  return p > 1;      // no divisor without remainder found
}

컴파일 과정 값이 필요한 문맥에서는 컴파일러는 컴파일 과정에 constexpr 함수를 계산하려고 하지만, 계산할 수 없다면 오류를 발생시키며, 그 외의 상황에서는 실행 시간 호출로 남겨둡니다.

8.3 부분 특수화로 실행 경로 선택

컴파일 도중에 부분 특수화를 사용해 다양한 구현 중 하나를 선택하는 것이 가능해집니다. 부분 특수화는 인자의 속성에 따라 어떤 함수 템플릿의 구현을 선택할지 결정하는 데 널리 쓰입니다. 또한 기본 템플릿과 그 외의 특수 상황을 위한 부분 특수화를 제공하는 방식도 쓸 수 있습니다.

 함수 템플릿은 부분 특수화를 제공하지 않기 때문에 제약 사항에 따라 함수 구현을 바꾸고 싶다면 다른 방식을 써야합니다.

  • 정적 함수를 갖는 클래스 사용
  • std::enable_if 사용
  • SIFNAE 특성 사용
  • 컴파일 과정 if 특성 사용

8.4 SIFNAE

함수 호출에 해당하는 후보군에 함수 템플릿이 있으면 컴파일러는 먼저 어떤 템플릿 인자가 쓰여야 하는지 결정한 후 인자를 치환하고 어느 것이 제일 잘 맞는지 평가합니다. 이 과정에서 이상하게 함수가 생성될 수도 있으며, 이런 경우에는 오류가 나지 않고 이를 무시하는데 이를 SIFNAE라고 합니다.

 치환 과정에서 필요에 의해 실행되는 인스턴스화 과정과는 다르게 필요하지 않을 수도 있는 잠재적인 인스턴스화까지도 치환 합니다.

// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N]) 
{ 
  return N;
}

// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t) 
{ 
  return t.size();
}
  1. 첫 번째 함수 템플릿은 파라미터를 T(&)[N]으로 선언했습니다. 파라미터가 T형의 요소를 N개 받는 배열이어야만 합니다.
  2. 두 번째 함수 템플릿은 제약 사항 없이 파라미터를 단순히 T로만 선언 했으나 반환형으로 T::size_type을 사용하기에 size_type이 있어야만 합니다.

원시 배열이나 문자열 리터럴 같은 경우에는 첫 번째에만, std::vector<>를 사용하는 경우는 두 번째 함수에만 일치합니다. 원시 포인터를 전달하면 두개 모두에 일치하지 않아 오류를 발생시킵니다. 만약 std::allocator<>를 전달하는 경우에는 size_type은 존재하지만 size()함수는 존재하지 않습니다. 이런 경우에는 len() 함수는 찾았지만 size()를 호출할 수 없어 오류가 발생합니다. 앞선 경우에는 다른 형식들을 위한 대체 함수의 len()을 구비한 경우, 앞선 두 함수에서 일치하는 경우를 찾지 못하면 이 대체 함수를 실행시키지만, std::allocator<>의 경우는 두 번째 함수로 연역되지만, size()를 호출하지 못한다는 에러만 발생시킵니다.

 이 SIFNAE를 적절히 사용하면, 특정 경우에 대해 해당 함수 템플릿을 오버로딩 해석에 참여하지 않도록 만들 수 있습니다. 이 기법을 상황마다 적용하는 것은 굉장히 어려울 수 있는데, 표준 라이브러리에서는 이를 쉽게하기 위하여 std:enable_if<>를 도입하였습니다. 이를 통해 앞선 문제가 해결 가능합니다.

template<typename T>
auto len(T const& t) -> decltype( (void)(t.size()), T::size_type() )
{
  return t.size();
};

이는 다음과 같은 패턴을 사용하였습니다

  • 반환형을 명시할 때 auto -> 형식을 사용합니다
  • decltype으로 반환형을 선언하고 쉼표 연산자를 사용합니다
  • 쉼표 연산자가 시작되는 시점에 유효해야 하는 모든 표현식을 작성합니다.
  • 실제 반환형의 객체를 쉼표 연산자의 끝에 정의합니다.

이는 decltype에 대해 쉼표 연산자를 통해서 앞의 (void)t.size()에 도출된 값을 넣어 존재 여부를 확인하고 T::size_type을 넣어 유효해야 하는 표현식을 넣는 형식으로 이전에 존재하는 지를 확인하는 과정이다.

8.5 컴파일 과정 if

C++17에서부터 컴파일 과정 조건에 따라 특정 명령문을 활성화/비활성화시킬 수 있게 하는 컴파일 과정 if문을 새로 추가했습니다. if constexpr(...)이라는 문법을 써서 컴파일 과정 표현식을 만들면 then 부분과 else 부분 중 어디를 쓸 것인지 결정할 수 있습니다.

template<typename T, typename... Types>
void print(T const& firstArg, Types const& ...args)
{
  std::cout << firstArg << '\n';
  if constexpr(sizeof...(args) > 0) {
    print(args...);
  }
}

코드가 인스턴스화되지 않는다는 것은 첫 번째 번역 단계만 실행되며, 올바른 문법과 이름을 사용했는지만 검사할 뿐 템플릿 파라미터를 사용하지 않습니다. 만약 문법이 맞지 않는다면 사용하지 않아도 오류를 발생시킵니다.

8.6 요약

  • 템플릿을 사용하면 컴파일 과정에 계산할 수 있습니다
  • constexpr 함수에서는 대부분의 컴파일 과정 계산을 컴파일 과정 문맥에서 호출 가능한 일반 함수로 바꿀 수 있습니다
  • 부분 특수화를 사용하면 컴파일 과정 제약 사항에 따라 클래스 템플릿의 여러 가지 구현 중 하나를 선택할 수 있습니다.
  • 템플릿은 필요할 때에만, 그리고 함수 템플릿 선언을 치환했을 때 코드가 유효할 때에만 사용됩니다.
  • SIFNAE는 정해진 형식이나 제약 사항하에서만 함수 템플릿을 제공하기 위해 사용되는 원칙입니다.
  • C++17에서부터는 컴파일 과정 if를 사용해 컴파일 과정 조건에 따라 명령문을 활성화하거나 비활성화할 수 있습니다.