Posts C++ more on move semantics
Post
Cancel

C++ more on move semantics

Forwarding references represent an lvalue reference if they are initialized by an lvalue and an rvalue reference if they are initialized by an rvalue. Rvalue references are forwarding references if they involve type deduction and appear exactly in the form T&& or auto&&.

1
2
3
4
template< typename T >
void f( T&& x ); // forwarding reference

auto&& var2 = var1; // forwarding reference

Considering:

1
2
3
4
5
6
7
8
9
template < typename T >
void foo( T&& ) {
    puts( "foo(T&&)" );
}

int main() {
    Widget w{};
    foo( w );
}

From all we have seen so far, this should not compile. w is an lvalue. An lvalue should not bind to an rvalue reference. But it does compile. And it prints foo( T&& ). In argument deduction, T&& is deduced to be Widget&. So we have Widget& &&, we are actually not allowed to make a reference to a reference, however the compiler can come up with this during type deduction. It uses something called reference collapsing to translate & && to &. Thus, the only way to get an rvalue from type deduction is to have && &&. So, finally the function is essentially deduced to something like:

1
2
3
4
template<  >
void foo( Widget& ) {
    puts( "foo(T&&)" );
}

What about rvalues?

1
2
3
4
5
6
7
8
template< typename T >
void foo(T&&) {
    puts("foo(T&&)");
}

int main() {
    foo(Widget{});
}

Again, this will work and print foo(T&&). This is again due to type deduction. The function parameter essentially becomes Widget&&.

Why is this in modern C++? Well, there was a problem prior to introduction of this concept. If we want to write a function that just forwards it’s arguments to another function then we have a problematic situation without this concept.

1
2
3
4
5
6
7
namespace std {
    template<typename T, ???>
    unique_ptr<T> make_unique(???) {
        return unique_ptr<T>(new T(???));
    }
}

This was actually unsolved prior to C++11. The first solution was to pass by value:

1
2
3
4
5
6
namespace std {
    template<typename T, typename arg>
    unique_ptr<T> make_unique(Arg arg) {
        return unique_ptr<T>(new T(arg));
    }
}

This works pretty often, but it does not work for non-copyable types and creates overhead. Okay, so usually in this case we pass by reference to avoid this. Let’s try:

1
2
3
4
5
6
namespace std {
    template<typename T, typename arg>
    unique_ptr<T> make_unique(Arg& arg) {
        return unique_ptr<T>(new T(arg));
    }
}

This works for const too. However, this all falls apart because we can not pass an rvalue. Something as simple as std::make_unique<int>(1); would not compile. An rvalue does not bind to an lvalue reference non const. So this does not work. Okay, so let’s add the reference to const:

1
2
3
4
5
6
namespace std {
    template<typename T, typename arg>
    unique_ptr<T> make_unique(Arg const& arg) {
        return unique_ptr<T>(new T(arg));
    }
}

This actually works for a lor of things. However, consider:

1
2
3
4
struct{ Example(int&); };

int i{1};
std::make_unique<Example>(i); // this will always add const

So this would also not compile. So prior to C++11 there was no general solution. This is exactly what forwarding references solves, hence the name.

1
2
3
4
5
6
namespace std {
    template<typename T, typename arg>
    unique_ptr<T> make_unique(Arg&& arg) {
        return unique_ptr<T>(new T(arg));
    }
}

This accepts anything and can be anything. A small additional problem we now have though is just because in this function it has a name, even if we pass an rvalue, it becomes an lvalue. So, we need to move conditionally. This was introduced by std::forward.

std:forward conditionally casts its input into an rvalue reference. If the given value is an lvalue, it casts it to an lvalue reference. If however, the given value is an rvalue, it casts it to an rvalue reference. std::forward does not forward anything, it again, just casts.

1
2
3
4
template<typename T>
T&& forward(std::remove_reference_t<T>& t) noexcept {
    return static_cast<T&&>(t);
}

So, we can use this:

1
2
3
4
5
6
namespace std {
    template<typename T, typename arg>
    unique_ptr<T> make_unique(Arg&& arg) {
        return unique_ptr<T>(new T(std::forward<Arg>(arg)));
    }
}

Notice we pass the type. If the type is an lvalue, we cast to lvalue reference, if rvalue, to rvalue reference. Now, this function works perfectly. We can enhance this by allowing to pass an arbitrary number of elements and all of them will be forwarding references:

1
2
3
4
5
6
namespace std {
    template<typename T, typename... Args>
    unique_ptr<T> make_unique(Args&&... args) {
        return unique_ptr<T>(new T(std::forward<Args>(args)...));
    }
}

There is a guideline that should be followed generally:

  • Effective Modern C++, Item 26: Avoid overloading on universal references (forwarding references)
This post is licensed under CC BY 4.0 by the author.

Recent Update

    Contents