Search code examples
c++c++17enable-ifperfect-forwarding

Is std::decay redundant in std::is_convertible?


I wrote such code:

template <class T>
class A {
  template <class U, class = 
class std::enable_if_t<std::is_convertible_v<std::decay_t<U>, std::decay_t<T>>>>
      void f(U&& val) {}
    };

I want that user of my class can call f only with types that convertible to T.

Is std::decay redundant there? If I remove it maybe I miss some special cases?


Solution

  • I think your question is more philosophical, as in: In the universe of types in C++, are there any cases of T and U where there is an observable difference between calling f() and g() on the following class:

    template <class T>
    struct A {
     
        template <
            class U, 
            enable_if_t<is_convertible_v<decay_t<U>, decay_t<T>>>* = nullptr
        >
        void f(U&& val) {}
    
        template <
            class U, 
            enable_if_t<is_convertible_v<U, T>>* = nullptr
        >
        void g(U&& val) {}
    };
    

    What does decay_t actually do?

    • remove top-level const/volatile qualifiers
    • remove top-level reference qualifiers
    • array -> pointer conversion
    • function -> function pointer conversion

    It may be worth noting: decay_t is modeled on what happens to function argument types when passed into a function.

    (UPDATED)

    If U is taken by value, then decay_t<U> will be equivalent to U.

    But since U is a universal reference (in your example), it is possible that U deduces to reference type, so decaying it will have a visible effect. When T can only be constructed from a reference, then decaying it will change the answer of this type trait.

    Merry's excellent example:

    #include <type_traits>
    #include <utility>
    
    template <class T>
    struct A {
     
        template <
            class U, 
            std::enable_if_t<std::is_convertible_v<std::decay_t<U>, T>>* = nullptr
        >
        void f(U&& val) {}
    
        template <
            class U, 
            std::enable_if_t<std::is_convertible_v<U, T>>* = nullptr
        >
        void g(U&& val) {}
    };
    
    struct B
    {
        B(int &) {}
    };
    
    void foo() {}
    
    int main() {
    
        A<B> a1;
        int x = 3;
        // a1.f(x);  // fails to compile: int not convertible to B
        a1.g(x);     // ok; int& is convertible to B
    }