IT박스

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

itboxs 2020. 6. 1. 19:24
반응형

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


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 만 사용할 수 있으므로 어디에서나 작동합니다.

For me, the readability aspect is the big motivating factor in this choice.


std::enable_if relies on the "Substition Failure Is Not An Error" (aka SFINAE) principle during template argument deduction. This is a very fragile language feature and you need to be very careful to get it right.

  1. if your condition inside the enable_if contains a nested template or type definition (hint: look for :: tokens), then the resolution of these nested tempatles or types are usually a non-deduced context. Any substitution failure on such a non-deduced context is an error.
  2. the various conditions in multiple enable_if overloads cannot have any overlap because overload resolution would be ambiguous. This is something that you as an author need to check yourself, although you'd get good compiler warnings.
  3. enable_if manipulates the set of viable functions during overload resolution which can have surprising interactions depending on the presence of other functions that are brought in from other scopes (e.g. through ADL). This makes it not very robust.

In short, when it works it works, but when it doesn't it can be very hard to debug. A very good alternative is to use tag dispatching, i.e. to delegate to an implementation function (usually in a detail namespace or in a helper class) that receives a dummy argument based on the same compile-time condition that you use in the 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 */) {}
}

Tag dispatching does not manipulate the overload set, but helps you select exactly the function you want by providing the proper arguments through a compile-time expression (e.g. in a type trait). In my experience, this is much easier to debug and get right. If you are an aspiring library writer of sophisticated type traits, you might need enable_if somehow, but for most regular use of compile-time conditions it's not recommended.


Which solution should be preferred and why should I avoid others?

  • The template parameter

    • It is usable in Constructors.
    • It is usable in user-defined conversion operator.
    • It requires C++11 or later.
    • It is IMO, the more readable.
    • It might easily be used wrongly and produces errors with overloads:

      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()
      

    Notice typename = std::enable_if_t<cond> instead of correct std::enable_if_t<cond, int>::type = 0

  • return type:

    • It cannot be used in constructor. (no return type)
    • It cannot be used in user-defined conversion operator. (not deducible)
    • It can be use pre-C++11.
    • Second more readable IMO.
  • Last, in function parameter:

    • It can be use pre-C++11.
    • It is usable in Constructors.
    • It cannot be used in user-defined conversion operator. (no parameters)
    • It cannot be used in methods with fixed number of arguments (unary/binary operators +, -, *, ...)
    • It can safely be use in inheritance (see below).
    • Change function signature (you have basically an extra as last argument void* = nullptr) (so function pointer would differ, and so on)

Are there any differences for member and non-member function templates?

There are subtle differences with inheritance and using:

According to the using-declarator (emphasis mine):

namespace.udecl

The set of declarations introduced by the using-declarator is found by performing qualified name lookup ([basic.lookup.qual], [class.member.lookup]) for the name in the using-declarator, excluding functions that are hidden as described below.

...

When a using-declarator brings declarations from a base class into a derived class, member functions and member function templates in the derived class override and/or hide member functions and member function templates with the same name, parameter-type-list, cv-qualification, and ref-qualifier (if any) in a base class (rather than conflicting). Such hidden or overridden declarations are excluded from the set of declarations introduced by the using-declarator.

So for both template argument and return type, methods are hidden is following scenario:

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() {}
};

Demo (gcc wrongly finds the base function).

Whereas with argument, similar scenario works:

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) {}
};

Demo

참고URL : https://stackoverflow.com/questions/14600201/why-should-i-avoid-stdenable-if-in-function-signatures

반응형