함수 서명에서 std :: enable_if를 피해야하는 이유


165

Scott Meyers는 다음 책 EC ++ 11의 내용과 상태 를 게시 했습니다. 그는이 책의 한 항목은 " std::enable_if기능 서명을 피하십시오 "라고 썼다 .

std::enable_if 함수 인수, 반환 형식 또는 클래스 템플릿 또는 함수 템플릿 매개 변수로 사용하여 오버로드 확인에서 함수 또는 클래스를 조건부로 제거 할 수 있습니다.

이 질문 에는 세 가지 솔루션이 모두 표시됩니다.

기능 매개 변수로 :

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

템플릿 매개 변수로 :

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

반환 유형으로 :

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • 어떤 솔루션을 선호해야하며 다른 솔루션을 피해야합니까?
  • " std::enable_if함수 서명을 피하십시오 " 는 반환 유형 (일반 함수 서명의 일부가 아닌 템플릿 전문화)으로 사용하는 것과 관련 이있는 경우 는 무엇입니까?
  • 멤버 및 비 멤버 함수 템플릿에 차이가 있습니까?

오버로딩은 일반적으로 훌륭하기 때문에. 있다면, (전문화 된) 클래스 템플릿을 사용하는 구현에 위임하십시오.
sehe

멤버 함수에는 과부하 세트 에 현재 과부하 선언 과부하가 포함된다는 점이 다릅니다 . variadics 지연 리턴 유형 (반환 유형이 다른 과부하에서 유추되는 경우)을 수행 할 때 특히 중요합니다.
sehe

1
음, 단지 주관적으로 나는 종종 매우 유용하면서 내가 좋아하지 않아 말을 std::enable_if내 함수 서명 (특히 추악한 추가 더럽 nullptr항상 뭔가에 대해 (그것이 무엇인지와 같은 이상한 해킹을 보이기 때문에 함수 인수 버전) static if힘 인터랙 팅 언어 기능을 활용하기 위해 템플릿 블랙 매직을 사용하여 훨씬 더 아름답고 깨끗하게 수행하십시오. 이것이 가능할 때마다 태그 디스 패칭을 선호하는 이유입니다 (여전히 이상한 인수가 있지만 공개 인터페이스에는 없으며 추악하고 암호가 적습니다 ).
Christian Rau

2
나는 무엇 =0typename std::enable_if<std::is_same<U, int>::value, int>::type = 0성취 하는지 묻고 싶 습니까? 나는 그것을 이해할 수있는 올바른 자료를 찾을 수 없었습니다. 전에 나는 첫 번째 부분을 알고있는 =0회원 유형이 int있는 경우를 Uint동일합니다. 많은 감사합니다!
astroboylrx

4
@astroboylrx Funny, 나는 이것을 지적하는 의견을 남기려고했습니다. 기본적으로 = 0은 이것이 기본이 아닌 유형이 아닌 템플릿 매개 변수 임을 나타냅니다 . 기본 형식 템플릿 매개 변수가 서명의 일부가 아니므로 오버로드 할 수 없으므로이 방법으로 수행됩니다.
Nir Friedman

답변:


107

해킹을 템플릿 매개 변수에 넣습니다 .

enable_if템플릿 매개 변수에의 접근 방법이 다른 방법에 비해 적어도 두 가지 장점이 있습니다 :

  • 가독성 : enable_if 사용 및 리턴 / 인수 유형이 하나의 지저분한 유형 이름 명확화 기 및 중첩 유형 액세스로 병합되지 않습니다. 명확성 및 중첩 유형의 혼란이 별칭 템플릿으로 완화 될 수는 있지만 관련이없는 두 가지 항목이 여전히 병합됩니다. enable_if 사용은 리턴 유형이 아닌 템플리트 매개 변수와 관련됩니다. 템플릿 매개 변수에 포함하면 중요한 것에 더 가깝습니다.

  • 보편적 적용 성 : 생성자에는 반환 유형이 없으며 일부 연산자에는 추가 인수를 사용할 수 없으므로 다른 두 옵션 중 어느 것도 적용 할 수 없습니다. 템플릿 매개 변수에 enable_if를 넣으면 템플릿에서 SFINAE 만 사용할 수 있으므로 어디에서나 작동합니다.

저에게는 가독성 측면이이 선택에서 가장 큰 동기 부여 요소입니다.


4
hereFUNCTION_REQUIRES 매크로를 사용하면 읽기가 훨씬 쉬워지고 C ++ 03 컴파일러에서도 작동 하며 리턴 유형 에서 사용 합니다. 또한 함수 템플릿 매개 변수를 사용하면 오버로드 문제가 발생합니다. 이제 함수 서명이 고유하지 않아서 오버로드 오류가 발생하기 때문입니다. enable_ifenable_if
Paul Fultz II

3
@Paul이 제기 한 문제에 대한 해결책 enable_if은 오버로드를 허용하는 기본 유형이 아닌 템플릿 매개 변수와 함께 사용 하는 것입니다. 즉 enable_if_t<condition, int> = 0대신 typename = enable_if_t<condition>.
Nir Friedman


@ R.MartinhoFernandes flamingdangerzone댓글 의 링크가 스파이웨어 설치 페이지로 연결되는 것으로 보입니다. 중재자의 관심을 끌기 위해 신고했습니다.
nispio

58

std::enable_if은 "에 의존 Substition의 실패가 아닌 오류 시 원칙 (SFINAE 일명)" 템플릿 인수 공제 . 이것은 매우 취약한 언어 기능이므로 제대로 작동하려면 매우주의해야합니다.

  1. 내부의 조건 enable_if에 중첩 된 템플릿 또는 유형 정의 (힌트 : ::토큰 찾기 ) 가 포함 된 경우 이러한 중첩 된 템플릿 또는 유형 의 해결은 일반적으로 교육 되지 않은 컨텍스트 입니다. 이러한 교육을받지 않은 컨텍스트에서 대체 실패는 오류 입니다.
  2. enable_if과부하 해결이 모호하기 때문에 여러 과부하 의 다양한 조건 이 겹칠 수 없습니다. 좋은 컴파일러 경고 메시지가 표시되지만 작성자로서 스스로 확인해야 할 사항입니다.
  3. enable_if다른 범위 (예 : ADL)에서 가져온 다른 기능의 존재 여부에 따라 놀라운 상호 작용을 가질 수있는 과부하 해결 중 실행 가능한 기능 세트를 조작합니다. 이것은 매우 강력하지 않습니다.

즉, 작동하면 작동하지만 작동하지 않으면 디버깅하기가 매우 어려울 수 있습니다. 매우 좋은 대안은 태그 디스패치 를 사용하는 것입니다 . 즉, 에서 사용 detail하는 것과 동일한 컴파일 시간 조건에 따라 더미 인수를받는 구현 함수 (일반적으로 네임 스페이스 또는 헬퍼 클래스) 에 위임 하는 것 enable_if입니다.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

태그 디스패치는 오버로드 세트를 조작하지 않지만 컴파일 타임 표현식 (예 : 유형 특성)을 통해 적절한 인수를 제공하여 원하는 함수를 정확하게 선택하는 데 도움이됩니다. 내 경험상 이것은 디버깅하고 올바르게 얻는 것이 훨씬 쉽습니다. 정교한 유형 특성의 주목받는 라이브러리 작성자 인 경우 enable_if어떻게 든 필요할 수 있지만 대부분의 정규 컴파일 시간 조건에서는 권장되지 않습니다.


22
태그 디스패치에는 한 가지 단점이 있습니다. 함수의 존재를 감지하는 특성이 있고 해당 함수가 태그 디스패치 방식으로 구현 된 경우 해당 멤버를 항상 존재로보고하고 잠재적 인 대체 실패 대신 오류가 발생합니다 . SFINAE는 주로 후보 세트에서 과부하를 제거하는 기술이며, 태그 디스패치는 두 개 이상의 과부하를 선택하는 기술입니다. 기능에 일부 중복이 있지만 동일하지 않습니다.
R. Martinho Fernandes

@ R.MartinhoFernandes 간단한 예를 들어 설명해 드리겠습니다 enable_if.
TemplateRex

1
@ R.MartinhoFernandes이 점을 설명하는 별도의 답변이 OP에 가치를 더할 수 있다고 생각합니다. :-) BTW, 같은 특성을 작성 is_f_able하는 것은 SFINAE를 사용할 수있는 라이브러리 작성자에게 유리한 점이 있지만, "정규적인"사용자와 특성을 부여하는 경우 is_f_able태그 전달이 더 쉽다고 생각합니다.
TemplateRex

1
@hansmaad 나는 귀하의 질문에 대한 간단한 답변을 게시했으며 대신 블로그 게시물에서 "SFINAE로 또는 SFINAE로"문제를 해결할 것입니다 (이 질문에 대해서는 약간의 주제가 아닙니다). 완료 할 시간이 생기면 말입니다.
R. Martinho Fernandes

8
SFINAE는 "취약"입니까? 뭐?
궤도에서 가벼움 경주

5

어떤 솔루션을 선호해야하며 다른 솔루션을 피해야합니까?

  • 템플릿 매개 변수

    • 생성자에서 사용할 수 있습니다.
    • 사용자 정의 변환 연산자에서 사용할 수 있습니다.
    • C ++ 11 이상이 필요합니다.
    • 더 읽기 쉬운 IMO입니다.
    • 쉽게 잘못 사용되어 과부하로 인해 오류가 발생할 수 있습니다.

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    typename = std::enable_if_t<cond>올바른 대신 통지std::enable_if_t<cond, int>::type = 0

  • 반환 유형 :

    • 생성자에서는 사용할 수 없습니다. (반품 타입 없음)
    • 사용자 정의 변환 연산자에는 사용할 수 없습니다. (공제 불가능)
    • C ++ 11 이전 버전을 사용할 수 있습니다.
    • 두 번째로 더 읽기 쉬운 IMO.
  • 마지막으로 함수 매개 변수에서 :

    • C ++ 11 이전 버전을 사용할 수 있습니다.
    • 생성자에서 사용할 수 있습니다.
    • 사용자 정의 변환 연산자에는 사용할 수 없습니다. (매개 변수 없음)
    • 그것은 인수의 고정 된 수의 방법을 사용할 수 없습니다 (단항 / 이항 연산자 +, -, *, ...)
    • 상속에 안전하게 사용할 수 있습니다 (아래 참조).
    • 함수 서명을 변경하십시오 (기본적으로 마지막 인수로 여분이 있습니다 void* = nullptr). 따라서 함수 포인터가 달라집니다.

멤버 및 비 멤버 함수 템플릿에 차이가 있습니까?

상속과 미묘한 차이점이 있습니다 using.

using-declarator(강조 광산) 에 따르면 :

namespace.udecl

using-declarator에 의해 도입 된 선언 세트는 using-declarator에서 이름에 대해 규정 된 이름 조회 ([basic.lookup.qual], [class.member.lookup])를 수행하여 설명됩니다 (설명 된대로 숨겨지는 기능 제외). 이하.

...

using-declarator가 기본 클래스에서 파생 클래스로 선언을 가져올 때 파생 클래스의 멤버 함수 및 멤버 함수 템플릿 이 동일한 이름, parameter-type-list, cv-를 사용하여 멤버 함수 및 멤버 함수 템플릿 재정의하거나 숨 깁니다. 기본 클래스 (충돌이 아닌 )의 자격 및 참조 한정자 (있는 경우 ). 이러한 숨겨 지거나 재정의 된 선언은 using-declarator가 도입 한 일련의 선언에서 제외됩니다.

따라서 템플릿 인수와 반환 유형 모두에 대해 다음과 같은 시나리오가 숨겨져 있습니다.

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

데모 (gcc가 기본 기능을 잘못 찾습니다).

논쟁의 여지가 있지만 비슷한 시나리오가 작동합니다.

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

데모

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.