1

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

jignatius
  • 6,304
  • 2
  • 15
  • 30
  • `T&&` in `impl(T&& b)` is not a forwarding reference, it is an rvalue one. The only way to get a forwarding reference is to make `impl` a template itself. – Evg Dec 30 '20 at 14:12
  • @Evg But `impl()` is a template function. – jignatius Dec 30 '20 at 14:15
  • `impl()` is not a template in your example, it is a non-template member function of a class template. – Evg Dec 30 '20 at 14:16
  • 1
    Only function template support perfect forwarding. If the function doesn't have `template<...>` before it, then the template parameter you are using is already known from instantiating the class so no perfect forwarding. – NathanOliver Dec 30 '20 at 14:18

1 Answers1

1
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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Makes sense. It's not perfect forwarding at all. It's just calling the function within the template class. – jignatius Dec 30 '20 at 14:23