Universal reference with templated class
Asked Answered
H

2

9

Example:

template <typename T>
class Bar
{
public:
    void foo(T&& arg)
    {
        std::forward<T>(arg);
    }
};

Bar<int> bar;    

bar.foo(10); // works

int a{ 10 };
bar.foo(a); // error C2664: cannot convert argument 1 from 'int' to 'int &&'

It seems that universal references works only with templated functions and only with type deduction, right? So it make no sense to use it with class? And does using of std::forward makes sense in my case?

Horatius answered 1/6, 2015 at 8:59 Comment(0)
T
14

Note that the preferred terminology (i.e. the one which will be in future versions of the spec) is now forwarding reference.

As you say, a forwarding reference only works with type deduction in a function template. In your case, when you say T&&, T is int. It can't be int& because it has been explicitly stated in your Bar instantiation. As such, reference-collapsing rules can't occur, so you can't do perfect forwarding.

If you want to do perfect forwarding in a member function like that, you need to have a member function template:

template <typename U>
void foo(U&& arg)
{
    std::forward<U>(arg); //actually do something here
}

If you absolutely need U to have the same unqualified type as T, you can do a static_assert:

template <typename U>
void foo(U&& arg)
{
    static_assert(std::is_same<std::decay_t<U>,std::decay_t<T>>::value, 
                  "U must be the same as T");
    std::forward<U>(arg); //actually do something here
}

std::decay might be a bit too aggressive for you as it will decay array types to pointers. If that's not what you want, you could write your own simple trait:

template <typename T>
using remove_cv_ref = std::remove_cv_t<std::remove_reference_t<T>>;

template <typename T, typename U>
using is_equiv = std::is_same<remove_cv_ref<T>, remove_cv_ref<U>>;

If you need a variadic version, we can write an are_equiv trait. First we need a trait to check if all traits in a pack are true. I'll use the bool_pack method:

namespace detail
{
    template<bool...> struct bool_pack;
    template<bool... bs>
    using all_true = std::is_same<bool_pack<bs..., true>, bool_pack<true, bs...>>;
}
template <typename... Ts>
using all_true = detail::all_true<Ts::value...>;

Then we need something to check if each pair of types in Ts... and Us... satisfy is_equiv. We can't take two parameter packs as template arguments, so I'll use std::tuple to separate them (you could use a sentinel node, or split the pack halfway through instead if you wanted):

template <typename TTuple, typename UTuple>
struct are_equiv;

template <typename... Ts, typename... Us>
struct are_equiv <std::tuple<Ts...>, std::tuple<Us...>> : all_true<is_equiv<Ts,Us>...>
{};

Then we can use this like:

static_assert(are_equiv<std::tuple<Ts...>,std::tuple<Us...>>::value, 
              "Us must be equivalent to Ts");
Thoracotomy answered 1/6, 2015 at 9:7 Comment(6)
Thank you. static_assert is good for me. Is there something similar I can use with variadic template arguments in standard library?Horatius
I'm not sure what you mean. Something similar to static_assert that takes multiple arguments?Thoracotomy
Exactly. I have a class with template <typename... T> and a function which I want to declare as foo(U&&... args). So I need to compare both T and U. I can write a helper, but maybe it's already done in stl. I just can't find it.Horatius
Updated my answer with a possible solution.Thoracotomy
Wow, that's looks really scary. Unfortunately I'm not able to understand this magic symbols (btw, can you please advise some good reference where I can learn this advanced stuff?). All I can say - it doesn't compiles with VS2013. It gives me an error 'std::is_same<remove_cv<_Ty>::type,remove_cv<remove_reference<Us>::type>::type>...'Horatius
I'm probably using some language features not supported in VS2013, or hitting a compiler bug. As far as references are concerned, the best way to understand this stuff is just to try it out, look at other posts tagged c++ and templates, etc.Thoracotomy
T
6

You're right : "universal references" only appear when the type of a deduced parameter is T&&. In your case, there is no deduction (T is known from the class), hence no universal reference.

In your snippet, std::forward will always perform std::move as arg is a regular rvalue reference.

If you wish to generate a universal reference, you need to make foo a function template :

template <typename T>
class Bar
{
public:
    template <typename U>
    void foo(U&& arg)
    {
        std::forward<U>(arg);
    }
};
Tribune answered 1/6, 2015 at 9:7 Comment(4)
It T is int& your conclusion doesn't hold, pr at least is ambiguous. Can you clarify what you are trying to say woth you say "they only appear", and demonstrate that forwarding references aren't super magic?Bimah
@Yakk To my knowledge, "universal (or forwarding) reference" is the name given to the intersection of type deduction, rvalue references and reference collapsing. I admittedly didn't consider the case where T itself is a reference. Is it still a forwarding reference if the forwarded object is always of reference type ?Tribune
Yakk's point is that if T is int&, arg will be of type int& as well, i.e. referencing collapsing rules will apply, but it's not a forwarding reference because it's not in a deduced context.Thoracotomy
And a relatively standard use is to deduce a type into a type. Use a helper function to produce T in deduction, instantiate foo_t<T>. Splits deduction from use, but...Bimah

© 2022 - 2024 — McMap. All rights reserved.