Search code examples
c++templatesperfect-forwarding

Perfect forwarding in a template class


I have a template class and within that class I have a static template function with universal/forwarding reference parameters. The idea is to perfect forward arguments to the function.

#include <iostream>
#include <type_traits>

//Type traits
template <typename T>
struct is_lreference_const
{
    static const bool value = std::is_lvalue_reference<T>::value && std::is_const<typename std::remove_reference<T>::type>::value;
};

template <typename T>
struct is_lvreference
{
    static const bool value = std::is_lvalue_reference<T>::value && !std::is_const<typename std::remove_reference<T>::type>::value;
};

struct Bar{};

template <class... Args>
struct FooClass;

//Perfect forward to FooClass::impl()
template <class... T>
inline void foo(T&&... args) {
  FooClass<T&&...>::impl(std::forward<T>(args)...);
}

template <typename T>
struct FooClass<T> {
    inline static void impl(T&& b) {
      if constexpr (is_lvreference<T>::value)
          std::cout << "T&" << std::endl;
      else if constexpr (is_lreference_const<T>::value)
          std::cout << "const T&" << std::endl;
      else if constexpr (std::is_rvalue_reference<T>::value)
          std::cout << "T&&" << std::endl;
      else 
          std::cout << "T" << std::endl;
  }
};

int main()
{
  const Bar b2;
  foo(b2);  
  foo(Bar{});
  Bar b;
  foo(b);
}

This all works fine and the output is as expected:

const T&
T&&
T&

Demo

However, if I was to change the foo() function like this:

template <class... T>
inline void foo(T&&... args) {
  FooClass<T...>::impl(std::forward<T>(args)...);
}

Notice that the FooClass class doesn't get a forwarding reference. Then the output is:

const T&
T
T&

The value category for the rvalue reference is not passed to the function impl() even though I'm using std::forward. Why does this happen? Is it because I'm not calling impl() in a deducible context as T has already been deduced for FooClass? How to avoid such an issue?

(This question is related to another one posted earlier today).

Edit

I changed FooClass<T>::impl() to be a template function. Then I found that else if constexpr (std::is_rvalue_reference<T>::value) was never true. I discovered ths was because rvalues are deduced to be of type T and not T&&. So I added some print functions and found that perfect forwarding now worked:

#include <iostream>
#include <type_traits>

//Type traits
template <typename T>
struct is_lreference_const
{
    static const bool value = std::is_lvalue_reference<T>::value && std::is_const<typename std::remove_reference<T>::type>::value;
};

template <typename T>
struct is_lvreference
{
    static const bool value = std::is_lvalue_reference<T>::value && !std::is_const<typename std::remove_reference<T>::type>::value;
};

struct Bar{};

template <class... Args>
struct FooClass;

//Perfect forward to FooClass::impl()
template <class... T>
inline void foo(T&&... args) {
  FooClass<T&&...>::impl(std::forward<T>(args)...);
}

template<typename T>
void printme(const T&) {
    std::cout << "constant lvalue reference" << std::endl;
}

template<typename T>
void printme(T&) {
    std::cout << "lvalue reference" << std::endl;
}

template<typename T>
void printme(T&&) {
    std::cout << "rvalue reference" << std::endl;
}


template <typename T>
struct FooClass<T> {
    template <typename Arg>
    inline static void impl(Arg&& b) {
      printme(std::forward<Arg>(b));
  }
};

int main()
{
  const Bar b2;
  foo(b2);  
  foo(Bar{});
  Bar b;
  foo(b);
}

Output:

constant lvalue reference
rvalue reference
lvalue reference

Demo


Solution

  • template <typename T>
    struct FooClass<T> {
      inline static void impl(T&& b) {
    

    FooClass<X>::impl takes an X&&, and in it T is X.

    If X is int&&, then T&& is int&& && which is int&& and T is int&&.

    If X is int, then T&& is int&& which is int&&, and T is int.

    Within impl, you query T, not b. So you get a different value if you pass FooClass an int or and int&&, even though the signature of FooClass<X>::impl is the same.

    The forward outside of the call to impl has zero impact on the code within impl; either the call succeeds, or it does not and you get a compilation error.

    You are not deducing the arguments to impl, you are explicitly setting the types.

    forward is nothing but a conditional move. It isn't magic.

    Probably you need to brush up on how std forward, template function argument deduction (especially when you deduce T&& parameters), and how reference collapsing works. If you don't understand that, you might treat std forward as some kind of magic, when it isn't.