Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Forwarding reference vs const lvalue reference in template code

I've recently been looking into forwarding references in C++ and below is a quick summary of my current understanding of the concept.

Let's say I have a template function footaking a forwarding reference to a single argument of type T.

template<typename T>
void foo(T&& arg);

If I call this function with an lvalue then T will be deduced as T& making the arg parameter be of type T& due to the reference collapsing rules T& && -> T&.

If this function gets called with an unnamed temporary, such as the result of a function call, then Twill be deduced as T making the arg parameter be of type T&&.

Inside foo however, arg is a named parameter so I will need to use std::forward if I want to pass the parameter along to other functions and still maintain its value category.

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

As far as I understand the cv-qualifiers are unaffected by this forwarding. This means that if I call foo with a named const variable then T will be deduced as const T& and hence the type of arg will also be const T& due to the reference collapsing rules. For const rvalues T will be deduced as const T and hence arg will be of type const T&&.

This also means that if I modify the value of arg inside foo I will get a compile time error if I did infact pass a const variable to it.

Now onto my question. Assume I am writing a container class and want to provide a method for inserting objects into my container.

template<typename T>
class Container
{
public:
    void insert(T&& obj) { storage[size++] = std::forward<T>(obj); }
private:
    T *storage;
    std::size_t size;
    /* ... */
};

By making the insert member function take a forwarding reference to obj I can use std::forward to take advantage of the move assignment operator of the stored type T if insert was infact passed a temporary object.

Previously, when I didn't know anything about forwarding references I would have written this member function taking a const lvalue reference: void insert(const T& obj).

The downside of this is that this code does not take advantage of the (presumably more efficient) move assignment operator if insert was passed a temporary object.

Assuming I haven't missed anything.

Is there any reason to provide two overloads for the insert function? One taking a const lvalue reference and one taking a forwarding reference.

void insert(const T& obj);
void insert(T&& obj);

The reason I'm asking is that the reference documentation for std::vectorstates that the push_back method comes in two overloads.

void push_back (const value_type& val);
void push_back (value_type&& val);

Why is the first version (taking a const value_type&) needed?

like image 256
JonatanE Avatar asked Aug 31 '17 20:08

JonatanE


2 Answers

You have to be careful about function templates, versus non-template methods of class templates. Your member insert is not itself a template. It's a method of a template class.

Container<int> c;
c.insert(...);

We can pretty easily see that T is not deduced on the second line, because it's already fixed to int on the first line, because T is a template parameter of the class, not the method.

Non-template methods of class templates, only differ from regular methods in one way, once the class has been instantiated: they aren't instantiated unless they are actually called. This is useful because it allows a template class to work with types, for which only some of the methods make sense (STL containers are full of examples like this).

The bottom line is that in my example above, since T is fixed to int, your method becomes:

void insert(int&& obj) { storage[size++] = std::forward<int>(obj); }

This is not a forwaring reference at all, but simply takes by rvalue reference, i.e. it only binds to rvalues. That is why you typically see two overloads for things like push_back, one for lvalues and one for rvalues.

like image 51
Nir Friedman Avatar answered Nov 14 '22 21:11

Nir Friedman


@Nir Friedman already answered the question, so I'm going to offer some additional advice.

If your Container class is not meant to store polymorphic types (which is common of containers, including std::vector and other similar STL containers), you can get away with simplifying your code, in the way you're trying to do in your original example.

Instead of:

void insert(T const& t) {
    storage[size++] = t;
}
void insert(T && t) {
    storage[size++] = std::move(t);
}

You could get perfectly correct code by writing the following instead:

void insert(T t) {
    storage[size++] = std::move(t);
}

The reason for this is that if the object is being copied in, t will be copy-constructed with the object provided, and then move-assigned into storage[size++], whereas if the object is being moved in, t will be move-constructed with the object provided, and then move-assigned into storage[size++]. So you've simplified your code at the cost of a single extra move-assignment, which many compilers will happily optimize out.

There is a major downside to this approach, though: If the object defines a copy-constructor and doesn't define a move-constructor (common for older types in legacy code), this results in double-copies in all cases. Your compiler might be able to optimize it away (because compilers can optimize to completely different code so long as the user-visible effects are unchanged), but maybe not. That could be a significant performance hit if you have to work with heavy objects that don't implement move-semantics. This is probably the reason STL containers don't use this technique (they value performance over brevity). But if you're looking for a way to reduce the amount of boilerplate code you write, and aren't worried about having to use "copy-only" objects, then this will probably work fine for you.

like image 44
Xirema Avatar answered Nov 14 '22 22:11

Xirema