void_t and trailing return type with decltype: are they completely interchangeable?

前端 未结 2 2154
一向
一向 2021-02-07 08:17

Consider the following, basic example based on void_t:

template>
struct S: std::false_type {};

template&l         


        
相关标签:
2条回答
  • 2021-02-07 08:43

    This is the metaprogramming equivalent of: should I write a function or should I just write my code inline. The reasons to prefer to write a type trait are the same as the reasons to prefer to write a function: it's more self-documenting, it's reusable, it's easier to debug. The reasons to prefer to write trailing decltype are the similar to the reasons to prefer to write code inline: it's a one-off that isn't reusable, so why put in the effort factoring it out and coming up with a sensible name for it?

    But here are a bunch of reasons why you might want a type trait:

    Repetition

    Suppose I have a trait I want to check lots of times. Like fooable. If I write the type trait once, I can treat that as a concept:

    template <class, class = void>
    struct fooable : std::false_type {};
    
    template <class T>
    struct fooable<T, void_t<decltype(std::declval<T>().foo())>>
    : std::true_type {};
    

    And now I can use that same concept in tons of places:

    template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
    void bar(T ) { ... }    
    
    template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
    void quux(T ) { ... }
    

    For concepts that check more than a single expression, you don't want to have to repeat it every time.

    Composability

    Going along with repetition, composing two different type traits is easy:

    template <class T>
    using fooable_and_barable = std::conjunction<fooable<T>, barable<T>>;
    

    Composing two trailing return types requires writing out all of both expressions...

    Negation

    With a type trait, it's easy to check that a type doesn't satisfy a trait. That's just !fooable<T>::value. You can't write a trailing-decltype expression for checking that something is invalid. This might come up when you have two disjoint overloads:

    template <class T, std::enable_if_t<fooable<T>::value>* = nullptr>
    void bar(T ) { ... }
    
    template <class T, std::enable_if_t<!fooable<T>::value>* = nullptr>
    void bar(T ) { ... }
    

    which leads nicely into...

    Tag dispatch

    Assuming we have a short type trait, it's a lot clearer to tag dispatch with a type trait:

    template <class T> void bar(T , std::true_type fooable) { ... }
    template <class T> void bar(T , std::false_type not_fooable) { ... }
    template <class T> void bar(T v) { bar(v, fooable<T>{}); }
    

    than it would be otherwise:

    template <class T> auto bar(T v, int ) -> decltype(v.foo(), void()) { ... }
    template <class T> void bar(T v, ... ) { ... }
    template <class T> void bar(T v) { bar(v, 0); }
    

    The 0 and int/... is a little weird, right?

    static_assert

    What if I don't want to SFINAE on a concept, but rather just want to hard fail with a clear message?

    template <class T>
    struct requires_fooability {
        static_assert(fooable<T>{}, "T must be fooable!");
    };
    

    Concepts

    When (if?) we ever get concepts, obviously actually using concepts is much more powerful when it comes to everything related to metaprogramming:

    template <fooable T> void bar(T ) { ... }
    
    0 讨论(0)
  • 2021-02-07 08:49

    I used both void_t and trailing decltype when I was implementing my own homebrew version of Concepts Lite (I succeded, by the way), which required creation of many additional type traits, most of which use Detection idiom in one way or another. I used void_t, trailing decltype and preceding decltype.

    As far as I understood, these options are logically equivalent, so an ideal, 100%-conforming compiler should produce the same result using all of them. The problem, however, is that a particular compiler may (and will) follow different patterns of instantiation in different cases, and some of these patterns can go way beyond internal compiler limits. For example, when I tried to make MSVC 2015 Update 2 3 detect presence of multiplication by the same type, the only solution that worked was preceding decltype:

        template<typename T>
        struct has_multiplication
        {
            static no_value test_mul(...);
    
            template<typename U>
            static decltype(*(U*)(0) *= std::declval<U>() * std::declval<U>()) test_mul(const U&);
    
            static constexpr bool value = !std::is_same<no_value, decltype(test_mul(std::declval<T>())) >::value;
        };
    

    Every other version produced internal compiler errors, though some of them worked fine with Clang and GCC. I also had to use *(U*)(0) instead of declval, because using three declval's in a row, though perfectly legal, was just to much for the compiler in this particular case.

    My bad, I forgot. Actually I used *(U*)(0) because declval produces rvalue-ref of type, which can't be assigned to, and that's why I used this. But everything else is still valid, this version worked where others didn't.

    So for now my answer would be: "they're identical, as long as your compiler thinks they are". And this is something you have to find out by testing. I hope that this will stop being an issue in the following releases of MSVC and others.

    0 讨论(0)
提交回复
热议问题