치춘짱베리굿나이스

[Rank 5] ft_containers - SFINAE, std::enable_if, std::is_integral 본문

42/42s Cursus

[Rank 5] ft_containers - SFINAE, std::enable_if, std::is_integral

치춘 2023. 2. 25. 12:31

SFINAE

Substitution Failure Is Not An Error 의 줄임말이다

Substitution (치환) Failure (실패) is Not An Error (는 오류 아님)

뭐 이런 두루뭉술한 말이 다 있지? 단어 뜻만 봐서는 도대체 무슨 말인지 모르겠다

그러므로 공부가 필요하다…

cppreference says:

SFINAE

"Substitution Failure Is Not An Error"

This rule applies during overload resolution of function templates: When substituting the explicitly specified or deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error.
This feature is used in template metaprogramming.

“치환 실패는 에러가 아님”

 

이 규칙은 템플릿 함수의 오버로드 해결 중에 사용됩니다. 입력된 템플릿 매개변수에 대하여 명시된 자료형 또는 추론된 자료형으로 치환이 실패했을 경우, 컴파일 에러가 발생하는 대신 해당 함수를 오버로드 후보군에서 제외시킵니다.

이 기능은 템플릿 메타프로그래밍에서 사용됩니다.

 

Explanation

Function template parameters are substituted (replaced by template arguments) twice:
- explicitly specified template arguments are substituted before template argument deduction
- deduced arguments and the arguments obtained from the defaults are substituted after template argument deduction

Substitution occurs in
- all types used in the function type (which includes return type and the types of all parameters)
- all types used in the template parameter declarations
- all expressions used in the function type (since C++11)
- all expressions used in a template parameter declaration (since C++11)
- all expressions used in the explicit specifier (since C++20)

설명

 

템플릿 함수의 매개 변수들은 (템플릿 인자로) 두 번 치환됩니다.

  • 명시적으로 지정된 템플릿 인자들은 템플릿 인자 추론 전에 치환됩니다
  • 추론된 인자들과 기본 생성자에서 가져온 인자들은 템플릿 인자 추론 이후에 치환됩니다

치환은 다음과 같은 상황에서 발생합니다.

  • 함수 타입에 사용된 모든 자료형 (반환값 자료형과 모든 매개변수 자료형 포함)
  • 템플릿 매개변수 선언 시에 사용된 모든 자료형
  • (C++11부터) 함수 자료형에 사용된 모든 표현식
  • (C++11부터) 템플릿 매개변수 선언에 사용된 모든 표현식
  • (C++20부터) explicit 한정자에 사용된 모든 표현식

 

A substitution failure is any situation when the type or expression above would be ill-formed (with a required diagnostic), if written using the substituted arguments.

Only the failures in the types and expressions in the immediate context of the function type or its template parameter types or its explicit specifier (since C++20) are SFINAE errors. If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors. A lambda expression is not considered part of the immediate context. (since C++20)

“치환 실패”는, 치환된 인자를 이용하여 코드를 작성할 경우 위의 경우에 해당하는 자료형과 표현식이 망가지게 되는 모든 상황을 일컫습니다.

오직 즉시 실행 컨텍스트에서의 자료형과 표현식, 템플릿 매개변수 자료형, explicit 한정자 (C++20부터) 에서의 자료형과 표현식 치환 실패만이 SFINAE로 간주됩니다. 만약 치환된 자료형 / 표현식이 ‘특정 템플릿 특수화에서의 인스턴스화’, ‘암시적으로 정의된 멤버 함수의 생성’ 등의 부작용을 발생시킬 경우, 이들은 하드 오류로 간주됩니다. 람다 표현식 (C++20부터) 는 즉시 실행 컨텍스트로 취급되지 않습니다.

 

Substitution proceeds in lexical order and stops when a failure is encountered.

If there are multiple declarations with different lexical orders (e.g. a function template declared with trailing return type, to be substituted after a parameter, and redeclared with ordinary return type that would be substituted before the parameter), and that would cause template instantiations to occur in a different order or not at all, then the program is ill-formed; no diagnostic required.

(since C++11)

치환은 사전식 순서로 진행되며, 중간에 실패할 경우 종료됩니다.

만약 서로 다른 사전식 순서를 가진 (예를 들면, (매개변수 이후에 치환되며, 매개변수 이전에 치환될 평범한 반환 값으로 재선언될) 후행 반환값 자료형과 함께 선언된 함수 템플릿) 여러 개의 선언문이 있고, 이로 인해 템플릿 인스턴스화의 순서가 달라지거나 아예 발생하지 않을 경우, 프로그램의 형식이 잘못되었다고 판단하며, 추론이 필요하지 않습니다

(C++11부터)

그래서 결론

템플릿 메타 프로그래밍에 사용되는 기능으로, 템플릿 인자로 들어온 “명시된 자료형” 또는 “추론된 자료형” 으로 치환이 실패했을 경우, 컴파일 에러를 발생시키는 대신 해당 함수는 오버로드 후보군에서 제외한다고 한다

쉽게 말해,

  • 트센 팀원들끼리 중국집에서 밥 시켜먹기로 함
  • 근데 내가 짜장면이나 짬뽕 안 시키고 돈까스나 비빔밥을 먹겠다고 해도
  • 자송이나 지소캉이 중식 안 주문했다고 화를 내거나 나를 쫒아내는 것이 아니라
  • 중국집을 오늘 저녁 메뉴 리스트에서 제외시킬 뿐

사실 맞는 비유인진 잘 모르겠음 그려려니 해주세요

함수 오버로드 찾는 순서와 SFINAE 예시

오버로드가 여러 종류 있을 때, 아래의 순서로 함수를 찾아 작동시킨다

1. 명시된 자료형 (explicitly specified)

void explicitly_specified_function(int n) {
    std::cout << n << "\n";
}

인자의 자료형이 정확하게 명시된 경우이다

위와 같은 경우, int (또는 그와 상응하는 자료형) 외에 들어올 수 없다

타입이 정확히 맞을 때 해당 오버로드가 호출된다는 뜻이다

 

explicitly_specified_function("안녕하소");

만약 위의 예시처럼 std::string 또는 char* 같은 전혀 관계없는 자료형의 변수가 인자로 넘어오면 해당 오버로드는 무시될 것이다

2. 템플릿 (template)

template <typename T>
void template_function(T n) {
    std::cout << n << "\n";
}

템플릿을 통해 타입을 추론하여 사용하는 경우이다

T 또는 그와 상응하는 (사용가능한 멤버 변수가 존재할 경우) 타입이 들어오면 해당 타입으로 추론되어 동작한다

 

template <typename T>
typename T::inner_type template_function(T n) {
    std::cout << "inner-type\n";
}

template_function("반갑소");

위와 같은 경우, 자료형 T를 가지고 반환값을 결정하고 있다

이때 “반갑소” (std::string 또는 char*) 를 인자로 넣으면,

  1. 컴파일러에서는 template_function(”반갑소”) 에서 템플릿을 이용하여 함수 코드를 생성한다 (인스턴스화)
  2. 어라? 근데 std::string (또는 char *)에는 inner_type 라는 멤버 변수가 없는뎁쇼
  3. 치환 실패!!!

허나 이런 경우에 치환을 실패했지만? 오류를 반환하지 않고 (같은 이름의) 다른 함수를 다시 탐색하는 것이다

말 그대로 “치환 실패는 오류가 아님” 이고, 단지 위의 template_function 이라는 함수를 함수 후보군에서 제외만 시켜주게 된다

3. 가변 인자

void variable_argument(...) {
    std::cout << "variable argument\n";
}

가장 후순위에 탐색하는 오버로드이다

위의 두 경우에 해당하는 오버로드들 중에 컴파일 가능한 오버로드가 없다면 여기로 내려오게 된다

enable_if

어디에 쓰는 것인지

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

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

위의 SFINAE 오버로드를 사용하기 위해 조건부로 자료형의 인스턴스를 만들어주는 클래스이다

템플릿 매개변수

enable_if 는 템플릿 매개 변수로 2개의 타입을 받는데,

  • Conditionboolean 값으로, 이 값이 truefalse냐에 따라 서로 다른 enable_if 오버로드가 호출된다
  • T는 인스턴스화 하여 반환할 값의 자료형 (타입) 이다

사용 방법

  • Conditiontrue 일 경우
    • enable_if 는 내부에 타입명을 define 해둔 typedef T type 가 존재한다
  • Conditionfalse일 경우
    • enable_if는 내부에 아무것도 갖지 않는다

이 두 차이와 SFINAE 특성을 이용해서 특정 인스턴스를 오버로드 후보군에서 제외하는 데 사용할 수 있다

한 함수를 오버로드 후보군에서 제외한다는 것은 즉, 다른 오버로드가 사용될 수 있도록 우선권을 넘겨주는 것과 같다

카드 패 (Condition) 를 보고 내 턴에 게임을 진행할 지 (현재 오버로드 사용) , 옆 사람에게 턴을 넘길 지 (현재 오버로드를 후보군에서 제외하고 다음 오버로드를 탐색) 결정한다고 생각하면 된다

이를 응용하여 정수 계열의 자료형들만 오버로딩할 수 있게끔 도와주는 것이 아래의 is_integral 클래스이다

is_integral

template <class T> // 베이스
struct is_itegral {
    static const bool value = false;
}

형식 T가 정수 계열인지 아닌지 판별하는 클래스이다

이때 정수 계열 여부를 판별하는 기준은 “cv-qualified 여부” 인데, 이를 충족시키는 자료형은 다음과 같다

  • bool
  • char, unsigned char, signed char
  • char16_t, char32_t, wchar_t
  • short int, unsigned short int
  • int, unsigned int
  • long int, unsigned long int
  • (가능한 경우) unsigned long int, unsigned long long int

선언형 및 템플릿 특수화

template <class T>
struct is_integral { // 베이스
    static const bool value = false;
};

위 오버로드는 is_integral의 베이스가 되는 오버로드로, 아래의 특수화된 자료형에 해당하지 않는 모든 자료형 T에 대해서 해당 오버로드가 호출된다

이 오버로드는 value 멤버변수를 들고 있으며, (템플릿 특수화를 제외한 모든 T에 대한) 기본값은 false이다

그렇다는 것은? is integral 이 false 이니 얘는 integral (= 정수형 자료형) 이 아니라는 뜻

template <>
struct is_integral <bool> { // 특수화됨
    static const bool value = true;
};

template <>
struct is_integral <char> { // 특수화됨
    static const bool value = true;
};

특수화가 된 인스턴스들이다

템플릿 특수화는 저런 식으로 template <class T> 가 있던 부분을 비우고, is_integral 에 명시적으로 타입을 넣는 식으로 수행된다

이렇게 되면, 모든 타입이 들어올 수 있었던 T와 달리, 정확히 저 타입 (bool, char) 만이 해당 오버로드를 호출하게 된다

이처럼 특정 자료형만 템플릿 T를 적용하지 않고 별도의 오버로드로 넘어가게 하는 기법을 템플릿 특수화라 하는데 일단 잠깐 짚고만 넘어가자

 

bool, char 의 경우, 내부의 멤버 변수 valuetrue이다

그렇다는 것은, 이 자료형들은 integral (정수형) 자료형이 맞다는 의미이다

이는 integral 계열 자료형에 대한 오버로드를 호출할 수 있게끔 하고, 그 외의 자료형들을 걸러줄 수 있게 한다

is_integral 특수화는 위의 cv-qualified 자료형에 대해 모두 오버로드 되어있다

사용 방법

template < class InputIterator >
size_type        calculate_size (
  InputIterator first,
  InputIterator last,
  typename ft::enable_if<!ft::is_integral<InputIterator>::value, InputIterator>::type* = NULL
) {
  size_type    i = 0;
  for (InputIterator it = first; it != last; it++)
    i++;
  return i;
}

나의 부끄러운 컨테이너 코드의 일부이다 (ft namespace는 내가 직접 짠 클래스들이라는 뜻)

calculate_size 는 3개의 인자를 받는데, 앞의 두 개는 벡터의 크기를 재기 위한 이터레이터이고, 뒤에 ft::enable_if 부분에 주목하자

위에서 템플릿 특수화를 통해 알아봤듯, ft::is_integral<InputIterator>::valueInputIterator가 정수형일 경우 true 를 반환한다

 

허나 우리가 사용하고자 하는 값은 반복자이지 정수형 값이 아니므로,

  • is_integral로 인해 InputIterator 가 정수형으로 판단될 경우
    • (ft::is_integral<InputIterator>::value == true)
  • enable_if의 첫 번째 인자가 false가 나오므로, 내부적으로 멤버 변수 type를 갖지 않게 되고
  • 따라서 type 에 인스턴스 값이 존재하지 않으므로 해당 오버로드는 무시된다

 

integral 자료형일 때 오버로드를 호출하는 데에도 도움을 주고, 반대로 integral 자료형일 때 해당 오버로드를 절대 호출하지 않도록 걸러주는 역할도 하는 것이다


참고자료

https://learn.microsoft.com/en-us/cpp/standard-library/enable-if-class?view=msvc-170

https://learn.microsoft.com/ko-kr/cpp/standard-library/is-integral-class?view=msvc-170

https://en.cppreference.com/w/cpp/types/is_integral

https://legacy.cplusplus.com/reference/type_traits/is_integral/?kw=is_integral

https://int-i.github.io/cpp/2020-03-25/cpp-sfinae/

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=hikari1224&logNo=221485187168

Comments