+Containers::enable_if and SIFNAE

YP J·2022년 9월 6일
0

ft_container

목록 보기
4/8

템플릿과 함수의 오버로딩

#include <iostream>

void foo(unsigned int i) 
{ std::cout << "unsigned " << i << "\n"; }

template <typename T>
void foo(const T& t) 
{
  std::cout << "template " << t << "\n";
}

int main()
{
  foo(42);
}
>>
template 42
  • 정수 리터럴의 경우 디폴트로 부호 있는 정수가 되기 때문에
  • (끝에 U 를 붙이지 않는 이상 부호 있는 정수가 됩니다),
  • 컴파일러가 오버로딩 후보들을 살펴볼 때 첫 번째 버전은 타입 변환이 필요하지만 (unsigned 를 붙여야 되죠),
  • 두 번째 버전은 타입 변환 없이 그냥 T 를 int 로 끼워넣으면 되기 때문에 결국 두 번째 후보가 채택됩니다.

    즉 컴파일이 메뉴얼대로 알아서 간단한걸 찾는다?

SIFNAE

  • 컴파일러가 템플릿으로 선언된 오버로딩 후보들을 살펴볼 때,
  • 템플릿 인자들의 타입들을 유추한 후에 이로 치환하는 과정에서 말도 안되는 코드를 생산할 때가 있습니다. 예를 들어서 아래의 예제를 살펴볼까요.
#include <iostream>

int negate(int i) { return -i; }

template <typename T>
typename T::value_type negate(const T& t) {
  return -T(t);
}

int main()
{
  std::cout << negate(42) << std::endl;
}
>> 
-42
  • 우선 컴파일러는 첫번째 후보군으로 T::value_type negate(const T& t) 로 가서
    • 아니!!!!!
    • 첫번째 후보군으로 들어가는게아니라 바로 int negate(int i) 함수로 들어간다
    • 왜냐면 위의 위의 예제에서 42 를 unsinged int 로 치환 해야해서 한단계 더 거치기 때문에 다른 후보군 부터 보는데
    • 이번 예제는 그렇지 않기 때문에 바로 int negate(int i) 로 들어간다.
  • T를 int 로 추론 하고
int ::value_type negate(const int& t) {
  /* ... */
}
  • 컴파일러는 이런 코드를 만든다

  • 하지만 int 에는 value_type 이라는 멤버가 없으므로 에러이다

  • 이러한 에러를 치환에러라 정의하고

  • 템플릿 인자 치환에 실패할 경우 (위 같은 경우)

  • 컴파일러는 이 오류를 무시하고,

  • "그냥 오버로딩 후보에서 제외하면 된다"

  • 그리고 두번째 후보로 ( 아니!! 첫번쨰로 얘 들어간다 위의 내용은 여기로 들어가면 이렇게 되었을것이다 라는 설명을 위해 한것? .)

int negate(int i) { return -i; }
  • 여기로 들어가서 negate(42)는 -42 가 되어서 나온다.

#include <iostream>

int negate(unsigned int i) { return -i; }

// #1
// template <typename T>
// typename T::value_type negate(const T& t) {
//   return -T(t);
// }

// #2
template <typename T>
void negate(const T& t) {
  typename T::value_type n = -t();
}
int main()
{
  std::cout << negate(42) << std::endl;
}
>> 
컴파일 에러 
  • int negate(unsigned int i) unsigned int 로 한단 계를 더 거치게 해서 template 후보군 부터 검사하게 했다.
  • 그러닌까 이번엔 컴파일 에러가 난다
  • #1 은 함수 타입과 템플릿 타입의 즉각적 맥락? (내 생각엔 생긴 모양) 이 같기 때문에 에러가 SFINAE 로 처리 되는데 (XXXXXXXXXXX)
 template <typename T>                     
 typename T::value_type negate(const T& t)  // 함수의 선언부
 {
   return -T(t);     // 정의부 
 }
  • 선언부에 T::value_type 이것 때문에 치환 오류가 발생시 -> SFINAE 로 처리 되어서 컴파일 에러 발생 하지 않는다.
  • 근데 #2 처럼 정의부에 typename T::value_type n = -t(); 로 치환 에러가 나면 그냥 컴파일 에러로 처리한다 ( SFINAE 범위밖 )

enable_if - 템플릿들을 위한 컴파일 타임 스위치

  • SFINAE 를 잘 활용하는 툴들중 가장 널리 쓰이는것이 바로 enable_if 이다.
#include <iostream>
#include <cstddef>

template <bool, typename T = void>
struct enable_if {};

template <typename T>
struct enable_if<true, T>
{
  typedef T type;
};

template <class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
void do_stuff(T& t)
{
  std::cout << "do_stuff integral\n";
}

template <class T, typename std::enable_if<std::is_class<T>::value, T>::type* = nullptr>
void do_stuff(T& t)
[
  // 일반적인 클래스 들을 받음
]
  • SFINAE 가 어떻게 여기서 활용되는지 보도록 하자
  • do_stuff(int 변수) 와 같이 함수를 호출하면
  • 컴파일러는 첫번째 오버로딩을 고르게 된다.
  • 왜냐면 std::is_integral<int\> 가 참 이기 때문이다.
  • 이때 두번째 오버로딩은 후보군에서 제외되는데 왜냐면
  • std::is_class<int\>가 false 이므로 내부에서 type이 정의 되지 않은 일반적인 형태의
  • struct enable_if 가 선택 되서 치환 오류가 발생하기 때문이다.

enable_if 사용 예제들

  • enable_if 없이는 템플릿 오버로딩 함수들이 무분별하게 오버로딩 되서 꾀나 골치아플 것입니다.
// Create the vector {8, 8, 8, 8}
std::vector<int> v1(4, 8);

// Create another vector {8, 8, 8, 8}
std::vector<int> v2(std::begin(v1), std::end(v1));

// Create the vector {1, 2, 3, 4}
int arr[] = {1, 2, 3, 4, 5, 6, 7};
std::vector<int> v3(arr, arr + 4);
  • 위와 같이 vector 의 경우 두 가지 형태의 두 개의 인자를 받는 생성자를 사용할 수 있습니다.
  • 할당자를 무시한다면, 위 생성자들은 아래와 같이 정의될 것입니다.
template <typename T>
class vector {
  vector(size_type n, const T val);

  template <class InputIterator>
  vector(InputIterator first, InputIterator last);

  ...
}
  • 위 두 생성자 모두 두 개의 인자를 받는데,

  • 두 번째의 경우 인자로 같은 타입인 두 객체가 전달된다면 오버로딩 됩니다. - 해당 생성자는 반복자를 받기 위해 만들어진 것이기에, 템플릿 인자 이름이 InputIterator 으로 되어 있지만 실제로는 어떠한 의미도 가지지 않습니다. - (그냥 그 자리에 T 가 와도 동작하는 방식은 똑같을 것입니다.)

  • 문제는 v1(4, 8) 과 같이 생성자를 호출하였을 경우, 프로그래머는 첫 번째 생성자를 의도한 것이기겠지만,

  • 실제로는 두 번째 생성자가 호출됩니다.

  • 왜냐하면 size_type 이 대개 unsigned 로 정의되어 있지만, 4 의 경우 그냥 signed 이므로 더 잘 맞는 후보군은 두 번째 것이 되기 때문이죠.

  • 따라서 라이브러리 제작자들은 이와 같은 상황을 피하기 위해 enable_if 를 이용해서 InputIterator 가 정말로 반복자일 때만 오버로딩 될 수 있도록 제한하였습니다.

template <class _InputIterator>
vector(
  _InputIterator __first,
  typename enable_if<__is_input_iterator<_InputIterator>::value &&
                     !__is_forward_iterator<_InputIterator>::value &&
                     /* ... more conditions... */ _InputIterator>::type __last);
  • 실제 생성자 소스코드.

  • 위 생성자의 경우 InputIterator 가 입력 반복자(input iterator) 이고 정방향 반복자(forward iterator)가 아닐 때 오버로딩 됩니다.

  • 반복자가 정방향 반복자일 경우 다른 오버로딩이 존재하는데, 왜냐하면 반복자가 정방향 반복자일 경우 vector 생성을 좀 더 효율적으로 할 수 있기 때문이죠.

  • 앞에서도 말했듯이 enable_if 는 C++ 11 표준 라이브러리 여기저기에서 사용되고 있습니다. string 의 append 함수 역시 위 예시와 매우 비슷한 용도로 사용되고 있습니다.


  • 다른 방식으로 enable_if 를 사용하는 예시로 std::signbit 함수를 들 수 있습니다. 이 함수는 모든 수 타입들(정수, 부동 소수점)에 대해 부호를 리턴하는 함수 입니다. 이 함수가 cmath 헤더에 어떻게 정의되어 있는지 간단히 보자면;
template <class T>
typename std::enable_if<std::is_arithmetic<T>, bool>::type signbit(T x) {
  // implementation
}
  • 만일 enable_if 를 사용하지 않았더라면,
  • 라이브러리 제작자들은 모든 수 타입들에 대해 일일히 오버로딩을 만들었어야만 했을 것입니다.
  • 또한 만약에 enable_if 없이 그냥 템플릿 함수로 만들었다면, 모든 타입에 대해 오버로딩이 가능하므로 골치아픈 오류를 만들었을 수 도 있습니다.
  • 하지만 enable_if 덕분에 귀찮은 코드 양을 크게 줄일 수 있었습니다.

모두의코드 작성자의 enable_if 에 대한 생각

  • 솔직히 말해서 std::enable_if 를 사용하는 코드를 보면 그리 깔끔하지 않습니다.

  • 만일 여러분이 enable_if 를 사용하게 된다면, 함수의 선언 부분과 리턴 타입을 쉽게 알아보기 힘들 것입니다.

  • 하지만 std::enable_if 는 코드 상에서 함수의 정의 부분 외에는 거의 사용되지 않습니다.

  • 마지막으로, C++ 표준 라이브러리에서는 비록 enable_if 가 코드를 좀 더 난잡하게 만들더라도,

  • 좀 더 복잡하지만 깔끔하게 나타내는 방식 대신에 알아보기 쉬운 enable_if 를 적극 사용하고 있습니다.

profile
be pro

0개의 댓글