Using alias templates for sfinae: does the languag

2019-03-09 22:00发布

问题:

I have just discovered the following technique. It looks very close to one of proposed concepts syntax, works perfectly on Clang, GCC and MSVC.

template <typename T, typename = typename std::enable_if<std::is_rvalue_reference<T&&>::value>::type>
using require_rvalue = T&&;

template <typename T>
void foo(require_rvalue<T> val);

I tried to find it with search requests like "sfinae in type alias" and got nothing. Is there a name for this technique and does the language actually allows it?


The full example:

#include <type_traits>

template <typename T, typename = typename std::enable_if<std::is_rvalue_reference<T&&>::value>::type>
using require_rvalue = T&&;

template <typename T>
void foo(require_rvalue<T>)
{
}

int main()
{
    int i = 0;
    const int ic = 0;
    foo(i);            // fail to compile, as desired
    foo(ic);           // fail to compile, as desired
    foo(std::move(i)); // ok
    foo(123);          // ok
}

回答1:

[...] does the language actually allows it?

Can't say anything about the name, but this seems to me to be a yes.

The relevant wording is [temp.alias]/2:

When a template-id refers to the specialization of an alias template, it is equivalent to the associated type obtained by substitution of its template-arguments for the template-parameters in the type-id of the alias template.

and the sfinae rule, [temp.deduct]/8:

Only invalid types and expressions in the immediate context of the function type, its template parameter types, and its explicit-specifier can result in a deduction failure.

Taking an argument of type require_rvalue<T> does behave as if we substitute that alias, which either gives us a T&& or a substitution failure - and that substitution failure is arguably in the immediate context of the substitution and so is "sfinae-friendly" as opposed to being a hard error. Note that even though the defaulted type argument is unused, as a result of CWG 1558 (the void_t rule), we got the addition of [temp.alias]/3:

However, if the template-id is dependent, subsequent template argument substitution still applies to the template-id.

This ensures that we still substitute into the defaulted type argument to trigger the required substitution failure.

The second unsaid part of the question is whether this actually can behave as a forwarding reference. The rule there is in [temp.deduct.call]/3:

A forwarding reference is an rvalue reference to a cv-unqualified template parameter that does not represent a template parameter of a class template (during class template argument deduction ([over.match.class.deduct])). If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

Is an alias template with one template parameter whose associated type is an rvalue reference to its cv-unqualified template parameter considered a forwarding reference? Well, [temp.alias]/2 says that require_rvalue<T> is equivalent to T&&, and T&& is the right thing. So arguably... yeah.

And all the compilers treat it as such, which is certainly a nice validation to have.


Although, note the existence of CWG 1844 and the lack of actual definition for immediate context, and the example there which also relies upon a substitution failure from a defaulted argument - which the issue states has implementation divergence.



回答2:

It works and allowed because it relays on widely used C++ features allowed by the standard:

  1. SFINAE in function parameters ([temp.over]/1, [temp.deduct]/6, [temp.deduct]/8):

    template <typename T>
    void foo(T&& v, typename std::enable_if<std::is_rvalue_reference<T&&>::value>::type* = nullptr)
    { /* ... */ }
    

    we cannot deduce on the actual parameter like void foo(typename std::enable_if<std::is_rvalue_reference<T&&>::value, T>::type&&) (CWG#549), but it is possible to workaround this limitation with template aliases (it is the trick I have presented in my question)

  2. SFINAE in template parameter declaration ([temp.deduct]/7):

    template <typename T, typename std::enable_if<std::is_rvalue_reference<T&&>::value>::type* = nullptr>
    void foo(T&& v)
    { /* ... */ }
    
  3. Alias templates in function parameters ([temp.alias]/2):

    template<class T> struct Alloc { /* ... */ };
    template<class T> using Vec = vector<T, Alloc<T>>;
    
    template<class T>
      void process(Vec<T>& v)
      { /* ... */ }
    
  4. Alias templates can have default parameters ([temp.param]/12, [temp.param]/15, [temp.param]/18)

  5. Template parameters of alias templates parameterized with deducible types still can be deduced ([temp.deduct.type]/17):

I have accepted @Barry's answer and put this one (with concentrated info and about every aspect the trick uses) because a lot of people (including me) are scared of C++ standard voodoo language about template deduction stuff.