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

前端 未结 2 2151
一向
一向 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 
    struct fooable : std::false_type {};
    
    template 
    struct fooable().foo())>>
    : std::true_type {};
    

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

    template {}>* = nullptr>
    void bar(T ) { ... }    
    
    template {}>* = 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 
    using fooable_and_barable = std::conjunction, barable>;
    

    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::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 ::value>* = nullptr>
    void bar(T ) { ... }
    
    template ::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  void bar(T , std::true_type fooable) { ... }
    template  void bar(T , std::false_type not_fooable) { ... }
    template  void bar(T v) { bar(v, fooable{}); }
    

    than it would be otherwise:

    template  auto bar(T v, int ) -> decltype(v.foo(), void()) { ... }
    template  void bar(T v, ... ) { ... }
    template  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 
    struct requires_fooability {
        static_assert(fooable{}, "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  void bar(T ) { ... }
    

提交回复
热议问题