함수 서명에서 std :: enable_if를 피해야하는 이유 가지 솔루션이 모두 표시됩니다. 기능 매개 변수로 : template<typename

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함수 서명을 피하십시오 “ 는 반환 유형 (일반 함수 서명의 일부가 아닌 템플릿 전문화)으로 사용하는 것과 관련 이있는 경우 는 무엇입니까?
  • 멤버 및 비 멤버 함수 템플릿에 차이가 있습니까?


답변

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

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

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

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

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


답변

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어떻게 든 필요할 수 있지만 대부분의 정규 컴파일 시간 조건에서는 권장되지 않습니다.


답변

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

  • 템플릿 매개 변수

    • 생성자에서 사용할 수 있습니다.
    • 사용자 정의 변환 연산자에서 사용할 수 있습니다.
    • 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) {}
};

데모


답변