C++11 Rvalue Reference Calling Copy Constructor Too

C++11 rvalue reference calling copy constructor too

Put noexcept on your move constructor:

TestClass(TestClass&& other) noexcept {

Elaboration: I was going to give this one Pierre, but unfortunately the cppreference source is only approximately correct.

In C++03

vector<T>::push_back(T)

has the "strong exception guarantee". That means that if the push_back throws an exception, the vector is left in the same state it had prior to the call to push_back.

This guarantee is problematic if the move constructor throws an exception.

When the vector reallocates, it would like to move the elements from the old buffer to the new. However if any one of those moves throws an exception (besides the first), then it is left in a state where the old buffer has been modified, and the new buffer doesn't yet contain everything it is supposed to. The vector can't restore the old buffer to its original state because it would have to move elements back to do so, those moves might also fail.

So a rule was laid down for C++11:

  1. If T has a noexcept move constructor, that can be used to move the elements from the old buffer to the new.

  2. Otherwise if T has a copy constructor, that will be used instead.

  3. Otherwise (if there is no accessible copy constructor), then the move constructor will be used after all, however in this case, the strong exception safety guarantee is no longer given.

Clarification: "copy constructor" in rule 2 means a constructor taking a const T&, not one of those weenie so-called T& copy constructors. :-)

Generic copy-constructor with rvalue-reference members

Here be dragons.

logger(logger const& other) : value_(other.value_)

The expression other.value_ is an lvalue of type T const, e.g. int&, int&& or int const.

  1. If T == int&&, you need to do a move, as the expression is an lvalue. The move is equivalent to a static_cast<int&&>, so you could do the static_cast directly as well.

  2. If T == int&, no cast is required.

  3. If T == int, no cast is required.

For a copy ctor defined as:

logger(logger const& other) : value_(static_cast<T>(other.value_)) {/*...*/}

Applied to the third case, this is defined as the introduction of a temporary, and could result in an additional copy/move, although I think it can&will be elided.

A solution without relying on the copy/move elision is to introduce a weird_cast, that yields the desired type in any case:

#include <type_traits>

template<class T, class U>
typename std::enable_if<std::is_reference<T>{}, T>::type
weird_cast(U& p)
{
return static_cast<T>(p);
}

template<class T, class U>
typename std::enable_if<not std::is_reference<T>{}, T const&>::type
weird_cast(U const& p)
{
return p;
}

int main()
{
int o = 42;
int & lo = o;
int && ro = std::move(o);
int const lco = o;

int&& r = weird_cast<int&&>(ro);
int& l = weird_cast<int& >(lo);
int d = weird_cast<int >(lco);
}

This is similar to std::forward, but also supports "forwarding" non-reference types.


Where are the dragons?

[class.copy]/11 specifies:

A defaulted copy/move constructor for a class X is defined as deleted if X has:

  • [...]
  • for the copy constructor, a non-static data member of rvalue reference type
  • [...]

An rvalue reference is typically bound to an xvalue or prvalue, i.e. to an expression referring to an object that is "near the end of its lifetime". As lifetime doesn't get extended through function boundaries, it would be error prone to allow such a "copying".

Overloading a function to use an rvalue reference instead of an lvalue reference

the code is the same for both since it's not a shallow copy, right?

The List(const List &rhs) copy constructor is expected to deep copy (not shallow copy) the data (not the nodes themselves) from rhs to this, but otherwise leave rhs intact and untouched.

The List(List &&rhs) move constructor is expected to move (steal) the nodes themselves from rhs to this and leave rhs in an empty state.

So, no, they would not be using the same code, not even close.

Same thing with the operator= copy assignment and move assignment operators (which are commonly implemented utilizing the copy/move constructors via the copy-swap idiom to avoid code duplication).

For example:

template<typename T>
class List
{
private:
struct node
{
T data;
node *previous;
node *next;
};

node *head = nullptr;
node *tail = nullptr;
size_t size = 0;

public:
// default constructor
List() = default;

// copy constructor
List(const List &rhs)
{
node **newNode = &head;
for(node *curNode = rhs.head; curNode; curNode = curNode->next)
{
*newNode = new node{curNode->data, tail, nullptr};
tail = *newNode;
++size;
newNode = &(tail->next);
}
}

// move constructor
List(List &&rhs)
: head(rhs.head), tail(rhs.tail), size(rhs.size)
{
rhs.head = rhs.tail = nullptr;
rhs.size = 0;
}

// destructor
~List()
{
node *curNode = head;
while (curNode) {
node *next = curNode->next;
delete curNode;
curNode = next;
}
}

// copy assignment
List& operator=(const List &rhs)
{
if (this != &rhs) {
List tmp(rhs);
std::swap(head, tmp.head);
}
return *this;
}

// move assignment
List& operator=(List &&rhs)
{
List tmp(std::move(rhs));
std::swap(head, tmp.head);
return *this;
}

/*
Alternatively, you can use just 1 implementation of operator=
for both copy and move assignments, by taking the input parameter
*by value* and letting the compiler decide which constructor
to call to initialize it, based on the type of input being assigned:

// copy/move assignment
List& operator=(List rhs)
{
List tmp(std::move(rhs));
std::swap(head, tmp.head);
return *this;
}
*/
};

An operation like L1 = L2 + L3 would have to implement operator+ that takes 2 List objects as input and returns a new List object with data (not nodes) that is copied from both L2 and L3, eg:

template<typename T>
class List
{
...
public:
...

List& operator+=(const List &rhs)
{
if (rhs.head) {
List tmp(rhs);
node **ptr = (tail) ? &(tail->next) : &head;
*ptr = tmp.head; tmp.head = nullptr;
tail = tmp.tail; tmp.tail = nullptr;
size += tmp.size;
}
return *this;
}

// operator+ can be implemented either as a class member
// using *this as the left-hand operand...
List operator+(const List &rhs) const
{
List res(*this);
res += rhs;
return res;
}
};

// Or, operator+ can be implemented as a non-member function...
List operator+(const List &lhs, const List &rhs)
{
List res(lhs);
res += rhs;
return res;
}

The returned List would be a temporary object, aka an rvalue, which would then be assigned to L1 using the operator=(List&&) move assignment operator (or, the operator=(List) assignment operator using the List(List&&) move constructor).

C++ constructor with rvalue reference

In order to take the rvalue reference, it should be non-const, since the contents of the constructor argument will be moved and typically this is an operation that changes the state of the operand (although not in your particular case):

Circle(Circle&& c){ }

Also, you are seeing a copy elision here:

Circle c3(Circle(4));

so the move constructor doesn't get invoked. This is a standard compiler optimization that may or may not happen. If you were to construct a Circle like this:

Circle c3(std::move(c1));

then you would invoke the move constructor.

C++11: shortest way to explicitly copy a value to use as an rvalue reference

template<class T>
std::remove_cv_t<T> copy(T& t) {
return t;
}
template<class T>
void copy(T&&)=delete; // block rvalue copy
template<class T, size_t N>
void copy(T(&)[N]) = delete; // we can implement this if we want: see below

will copy any lvalue. On rvalues and non-copyable types (like arrays) it will fail to match or compile.

template<class T>
std::decay_t<T> copy_even_if_rvalue(T&& t) {
return std::forward<T>(t);
}
namespace details {
template<class T, size_t N, size_t...Is>
std::array<T, N> copy_even_if_rvalue(T(&arr)[N], std::index_sequence<Is...>)
{
return {{arr[Is]...}};
}
}
template<class T, size_t N>
std::array<T, N> copy_even_if_rvalue(T(&arr)[N]) {
return details::copy_even_if_rvalue(arr, std::make_index_sequence<N>{} );
}
template<class T, size_t N>
std::array<T, N> copy_even_if_rvalue(T(&&arr)[N]) {
return copy_even_if_rvalue(arr); // forward to lvalue version
}

will copy both rvalues and lvalues. Usually copy is a smarter operation than copy_even_if_rvalue in my opinion.

copy_even_if_rvalue will convert raw C arrays into std::arrays, because that is about the only sensible way to copy them. An =delete overload would be the other reasonable option.

C++11 - Why compiler does not optimize rvalue reference to const lvalue reference binding?

Think about the following example:

void foo(int) {}
void foo(double) {}

void bar(double x) {
foo(x);
}

int main() {
bar(0);
}

In the above program, foo(double) will always be called, not foo(int). This is because while the argument was originally an int, this information is irrelevant once you are inside bar. bar only sees its own parameter x, which has type double regardless of what the original argument type was. Therefore, it calls the overload of foo that is the best match for the type of the parameter x.

Your func2 works similarly:

void func2(MyClass &el, const MyClass &c) {
el = c;
}

Here, the expression c is an lvalue, even though the reference may have been bound to a temporary object at the time of calling. Because of that, the compiler must select the = operator that takes an lvalue as its right argument.

In order to forward lvalues as lvalues and rvalues as rvalues, overloading with const MyClass& and MyClass&& is often used, even though (as you note) it is duplicative. For some suggestions on how to reduce code duplication, see Is There A Way To Remove Duplicate Code While Providing lvalue and rvalue Overloads? and How do I prevent code repeat between rvalue and lvalue member functions?

Copy constructor is being called first instead of only move constructor on passing even R-value reference

An moving assignment (not an assignment initialization) requires a move operator=(strings&&). It is not available if you defined own copy constructor. Note, that in situation like this

struct S {
S() {}
S(const S &obj) { std::cout << "Copy ctor" << std::endl;}
S(S &&obj) { std::cout << "Move ctor" << std::endl;}
S& operator=(const S &obj) { std::cout << "Copy =" << std::endl; return *this;}
S& operator=(S &&obj) { std::cout << "Move =" << std::endl; return *this;}
};

int main()
{
S a = S();
a = S();
}

with C++11 compiler the declaration line wouldn't even produce output, because no copy or move did happen, it was elided.



Related Topics



Leave a reply



Submit