C++ enable_if


std::enable_if 는 부울 조건을 사용하여 SFINAE를 트리거하는 편리한 유틸리티입니다. 그것은 다음과 같이 정의됩니다 :

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

template <typename Result>
struct enable_if<true, Result> {
    using type = Result;
};

즉, enable_if<true, R>::typeR 의 별칭입니다. enable_if<false, T>::typeenable_if 특수화에 type 멤버 유형이 없으므로 enable_if<false, T>::type 이 잘못되었습니다.

std::enable_if 는 템플릿을 제한하는 데 사용할 수 있습니다.

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

template <class F>
auto negate(F f) { return -f(); }

여기서, negate(1) 대한 호출은 모호함으로 인해 실패합니다. 하지만 두 번째 오버로드는 정수 유형에 사용되지 않으므로 다음을 추가 할 수 있습니다.

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

template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }

이제! negate<int> 인스턴스화하면 !std::is_arithmetic<int>::valuefalse 이므로 대체 오류가 발생합니다. SFINAE로 인해 이것은 심각한 오류는 아니지만이 후보는 단순히 오버로드 집합에서 제거됩니다. 결과적으로 negate(1) 에는 실행 가능한 후보자가 하나뿐입니다. 그러면이 후보가 호출됩니다.

언제 사용 하는가?

std::enable_ifstd::enable_if 위에 헬퍼 되지만 SFINAE가 처음부터 작동하게하는 것은 아닙니다. std::size 와 비슷한 기능을 구현하는 두 가지 대안, 즉 컨테이너 또는 배열의 크기를 생성하는 오버로드 세트 size(arg) 를 고려해 보겠습니다.

// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);

// implementation omitted
template<typename Cont>
struct is_sizeable;

// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);

is_sizeable 이 적절히 작성되었다고 가정하면이 두 선언은 SFINAE와 정확히 동일해야합니다. 어느 것이 가장 쓰기 쉽고 한 번에 리뷰하고 이해하는 것이 가장 쉽습니다.

이제 랩 어라운드 또는 모듈 식 동작을 위해 부호있는 정수 오버플로를 피하는 산술 헬퍼를 구현하는 방법을 고려해 보겠습니다. incr(i, 3) 예를 들어 incr(i, 3)i += 3 과 같을 것입니다. 사실 iINT_MAX 값을 가진 int 경우에도 결과가 항상 정의된다는 사실을 알 수 있습니다. 다음 두 가지 가능한 대안이 있습니다.

// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;

// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
 
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
 
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);

다시 한번 말하면 가장 쓰기 쉽고 한 눈에 검토하고 이해하는 것이 가장 쉽습니다.

std::enable_if 의 강점은 리팩토링 및 API 디자인에서 어떻게 사용되는지입니다. is_sizeable<Cont>::valuecont.size() 가 유효한지 여부를 반영하려는 경우 size1 대해 표시되는 표현식을 사용하는 것이 더 간결해질 수 있지만 is_sizeable 이 여러 위치에서 사용되는지 여부에 따라 달라질 수 있습니다. . 그것의 구현이 incr1 의 선언으로 유출 될 때보 다 훨씬 더 명확하게 의도를 반영하는 std::is_signed 와 대조를 incr1 .