Perfect forwarding with class template argument de

2019-03-21 06:48发布

问题:

I would like to understand how deductions guides work with universal references and std::forward, in particular to create perfectly forwarding wrappers. The code below provides a code to experiment with a functor wrapper in two cases: one with an implicit deduction guide and one with an explicit deduction guide.

I have put a lot of && and std::forward in comments, because I do not know where they are needed to achieve perfect forwarding. I would like to know where to put them, and where they are not needed.

// Case with not conversion constructor
template <class F>
struct functor1
{
    explicit constexpr functor1(F/*&&*/ f) 
    noexcept(std::is_nothrow_copy_constructible_v<F/*&&*/>)
    : _f(/*std::forward<F>(*/f/*)*/) 
    {}
    template <class... Args>
    constexpr operator()(Args&&... args) 
    noexcept(std::is_nothrow_invocable_v<F/*&&*/, Args/*&&*/...>)
    {
        /*std::forward<F>(*/_f/*)*/(std::forward<Args>(args)...);
    }
    private: F/*&&*/ _f;
};

// Case with a conversion constructor
template <class F>
struct functor2
{
    template <class G>
    explicit constexpr functor2(G&& g) 
    noexcept(std::is_nothrow_constructible_v<G/*&&*/, F/*&&*/>)
    : _f(/*std::forward<G>(*/g/*)*/) 
    {}
    template <class... Args>
    constexpr operator()(Args&&... args) 
    noexcept(std::is_nothrow_invocable_v<F/*&&*/, Args/*&&*/...>)
    {
        /*std::forward<F>(*/_f/*)*/(std::forward<Args>(args)...);
    }
    private: F/*&&*/ _f;
};
template <class G>
functor2(G&&) -> functor2<G/*&&*/>;

EDIT: For the sake of simplicity, and because it is not the point of the question, in the preceding examples, we consider that F and G are function objects ie classes/structs with an operator().

回答1:

In the C++ standard defines the term forwarding reference. A suppose universal reference is used as a synonym for this term. [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.

This concept only apply to template function argument or template constructor argument. In all other cases, T&& is a rvalue reference. The concept of forwarding reference is only usefull for template argument deduction. Let's consider that in the following examples, all the fonctions and constructors are called with an int argument (independently of its constness and value categories (lvalue/rvalue):

//possibilities of argument deduction, [cv] means any combination of "const" and "volatile": 
//  <"","const","volatile","const volatile">
template<class T> void f(T&);
  //4 possibilities: void f([cv] int&);

template<class T> void f(const T&);
  //2 possibilities: void f(const int&);
                   //void f(const volatile int&);

template<class T> void f(T&&);
  //Forwarding reference, 8 possibilities
            //void f([cv] int&);
            //void f([cv] int&&);

template<class T> void f(const T&&);
  //NOT a forwarding reference because of the const qualifier, 2 possibilities:
            //void f(const int&&);
            //void f(const volatile int&&);

template<class T>
struct S{
    template<class U>
    S(U&&);
      //Forwarding reference, 8 posibilities:
            //void S<X>([cv] int&);
            //void S<X>([cv] int&&);
      //no template argument deduction posible

    S(T&&);
      //NOT a forwarding reference, 1 possibility:
            //void S<X>(X&&);
      //Generated argument deduction:
         //template<class T> S(T&&) -> S<T>;
           //not a forwarding reference because T is a parameter of the template class; 
           //=> 4 possibilities: -> S<[cv] int&&>


    T&& a; //an rvalue reference if T is [cv] int or [cv] int&&,
           //an lvalue reference if T is [cv] int&;
           //This comes from reference colapsing rules: &+&=&; &&+&=&; &&+&&=&&       //(Nota: You may consider that a rvalue reference data member is probably a mistake)
 };

template<class U>
S(U&&) -> S<U&&>;
 //Forwarding reference, 8 possibilities:
 //   S<[cv] int&>;
 //   S<[cv] int&&>;

Using std::forward make sense only inside the body of a function or a constructor if the argument of std::forward can either be a rvalue reference or a lvalue reference, depending on template argument deduction and reference collapsing rules. If std::forward's argument always results in a rvalue reference, std::move is prefered, and if it always results in a lvalue reference, nothing is prefered.