types_enable_if

types_enable_if (enable_if 타입)

이 페이지는 C++ 표준 라이브러리의 std::enable_if 메타함수에 대해 설명해요. std::enable_if는 컴파일 타임에 불리언 조건에 따라 타입을 선택하거나, SFINAE를 활용해 함수 오버로드 후보에서 조건부로 함수를 제외하는 데 사용돼요. C++20의 concepts가 도입되기 전에 널리 쓰인 중요한 기법이에요.

출처: cppreference

본문

<type_traits> 헤더에 정의되어 있어요.

정의
template < bool B , class T = void > struct enable_if ; (C++11부터)

만약 Btrue라면, std::enable_ifT와 같은 공용 멤버 typedef type을 가져요. 그렇지 않으면 type 멤버가 존재하지 않아요.

이 메타함수는 C++20의 concepts 이전에 SFINAE를 활용하는 편리한 방법이에요. 특히 타입 특성(type traits)에 기반하여 함수를 후보 집합에서 조건부로 제거하고, 서로 다른 타입 특성에 따라 별도의 함수 오버로드나 특수화를 허용하는 데 사용돼요.

std::enable_if는 여러 형태로 사용할 수 있어요:

  • 추가 함수 인자로 사용 (대부분의 연산자 오버로드에는 적용 불가)
  • 반환 타입으로 사용 (생성자와 소멸자에는 적용 불가)
  • 클래스 템플릿 또는 함수 템플릿 매개변수로 사용

프로그램에서 std::enable_if에 대한 특수화를 추가하면 동작이 정의되지 않아요.

멤버 타입

타입 정의
type B의 값에 따라 T이거나 그러한 멤버가 없음

헬퍼 타입

template < bool B , class T = void > using enable_if_t = typename enable_if < B , T >:: type ; (C++14부터)

가능한 구현

template < bool B , class T = void > struct enable_if {}; template < class T > struct enable_if < true , T > { typedef T type ; };

참고 사항

흔한 실수는 기본 템플릿 인자만 다른 두 함수 템플릿을 선언하는 거예요. 이는 동작하지 않아요. 왜냐하면 그 선언들은 같은 함수 템플릿의 재선언으로 처리되기 때문이에요 (기본 템플릿 인자는 함수 템플릿 동등성에 포함되지 않아요).

/* WRONG */

struct T
{
    enum { int_t, float_t } type;

    template<typename Integer,
             typename = std::enable_if_t<std::is_integral<Integer>::value>>
    T(Integer) : type(int_t) {}

    template<typename Floating,
             typename = std::enable_if_t<std::is_floating_point<Floating>::value>>
    T(Floating) : type(float_t) {} // error: treated as redefinition
};

/* RIGHT */

struct T
{
    enum { int_t, float_t } type;

    template<typename Integer,
             std::enable_if_t<std::is_integral<Integer>::value, bool> = true>
    T(Integer) : type(int_t) {}

    template<typename Floating,
             std::enable_if_t<std::is_floating_point<Floating>::value, bool> = true>
    T(Floating) : type(float_t) {} // OK
};

네임스페이스 범위 함수 템플릿의 비타입 템플릿 매개변수 타입에 enable_if를 사용할 때는 주의해야 해요. Itanium ABI와 같은 일부 ABI 명세는 비타입 템플릿 매개변수의 인스턴스화 의존 부분을 name mangling에 포함하지 않아요. 즉, 서로 다른 두 함수 템플릿의 특수화가 같은 mangled name을 가질 수 있고, 잘못 링크될 수 있어요. 예를 들어:

// first translation unit

struct X
{
    enum { value1 = true, value2 = true };
};

template<class T, std::enable_if_t<T::value1, int> = 0>
void func() {} // #1

template void func<X>(); // #2

// second translation unit

struct X
{
    enum { value1 = true, value2 = true };
};

template<class T, std::enable_if_t<T::value2, int> = 0>
void func() {} // #3

template void func<X>(); // #4

함수 템플릿 #1과 #3은 서로 다른 시그니처를 가지며 별개의 템플릿이에요. 그럼에도 불구하고 #2와 #4는 서로 다른 함수 템플릿의 인스턴스화임에도 Itanium C++ ABI에서 같은 mangled name (_Z4funcI1XLi0EEvv)을 가져요. 따라서 링커는 이들을 동일한 엔티티로 잘못 간주하게 돼요.

예제

#include <iostream>
#include <new>
#include <string>
#include <type_traits>

namespace detail
{ 
    void* voidify(const volatile void* ptr) noexcept { return const_cast<void*>(ptr); } 
}

// #1, enabled via the return type
template<class T>
typename std::enable_if<std::is_trivially_default_constructible<T>::value>::type 
    construct(T*) 
{
    std::cout << "default constructing trivially default constructible T\n";
}

// same as above
template<class T>
typename std::enable_if<!std::is_trivially_default_constructible<T>::value>::type 
    construct(T* p) 
{
    std::cout << "default constructing non-trivially default constructible T\n";
    ::new(detail::voidify(p)) T;
}

// #2
template<class T, class... Args>
std::enable_if_t<std::is_constructible<T, Args&&...>::value> // Using helper type
    construct(T* p, Args&&... args) 
{
    std::cout << "constructing T with operation\n";
    ::new(detail::voidify(p)) T(static_cast<Args&&>(args)...);
}

// #3, enabled via a parameter
template<class T>
void destroy(
    T*, 
    typename std::enable_if<
        std::is_trivially_destructible<T>::value
    >::type* = 0)
{
    std::cout << "destroying trivially destructible T\n";
}

// #4, enabled via a non-type template parameter
template<class T,
         typename std::enable_if<
             !std::is_trivially_destructible<T>{} &&
             (std::is_class<T>{} || std::is_union<T>{}),
             bool>::type = true>
void destroy(T* t)
{
    std::cout << "destroying non-trivially destructible T\n";
    t->~T();
}

// #5, enabled via a type template parameter
template<class T,
	 typename = std::enable_if_t<std::is_array<T>::value>>
void destroy(T* t) // note: function signature is unmodified
{
    for (std::size_t i = 0; i < std::extent<T>::value; ++i)
        destroy((*t)[i]);
}

/*
template<class T,
	 typename = std::enable_if_t<std::is_void<T>::value>>
void destroy(T* t) {} // error: has the same signature with #5
*/

// the partial specialization of A is enabled via a template parameter
template<class T, class Enable = void>
class A {}; // primary template

template<class T>
class A<T, typename std::enable_if<std::is_floating_point<T>::value>::type>
{}; // specialization for floating point types

int main()
{
    union { int i; char s[sizeof(std::string)]; } u;

    construct(reinterpret_cast<int*>(&u));
    destroy(reinterpret_cast<int*>(&u));

    construct(reinterpret_cast<std::string*>(&u), "Hello");
    destroy(reinterpret_cast<std::string*>(&u));

    A<int>{}; // OK: matches the primary template
    A<double>{}; // OK: matches the partial specialization
}

출력:

default constructing trivially default constructible T
destroying trivially destructible T
constructing T with operation
destroying non-trivially destructible T

같이 보기

void_t (C++17) void 가변 인자 별칭 템플릿 (별칭 템플릿) [edit]
  • static_assert
  • SFINAE
  • Constraints and Concepts (제약과 개념)

더 알아보기 (Learn more)

cppreference