Sh3ljohn Sh3ljohn - 2 months ago 12
C++ Question

How would move semantics improve "my way"?

Background



I read the following answers earlier today, and it felt like relearning C++, litterally.

What is move semantics?

What is the copy-and-swap idiom?

Then I wondered if I should change my "ways" to use these exciting features; the main concerns I have are for code efficiency and clarity (former slightly more important than the latter to me). This lead me to this post:

Why have move semantics?

with which I strongly disagree (I agree with the answer, that is); I don't think a smart use of pointers could ever make move semantics redundant, neither in terms of efficiency nor clarity.

Question



Currently, whenever I implement a non-trivial object, I roughly do this:

struct Y
{
// Implement
Y();
void clear();
Y& operator= ( const& Y );

// Dependent
~Y() { clear(); }

Y( const Y& that )
: Y()
{
operator=(that);
}

// Y(Y&&): no need, use Y(const Y&)
// Y& operator=(Y&&): no need, use Y& operator=(const Y&)
};


From what I understand from the two first posts I read today, I am wondering whether it would be beneficial to change to this instead:

struct X
{
// Implement
X();
X( const X& );

void clear();
void swap( X& );

// Dependent
~X() { clear(); }

X( X&& that )
: X()
{
swap(that);
// now: that <=> X()
// and that.~X() will be called shortly
}

X& operator= ( X that ) // uses either X( X&& ) or X( const X& )
{
swap(that);
return *this;
// now: that.~X() is called
}

// X& operator=(X&&): no need, use X& operator=(X)
};


Now, aside from being slightly more complicated and verbose, I don't see a situation in which the second (
struct X
) would yield a performance improvement, and I find that it is also less readable. Assuming my second code is using move-semantics correctly, how would it improve my current "way" of doing things (
struct Y
)?




Note 1: The only situation which I think makes the latter clearer is for "moving out of function"

X foo()
{
X automatic_var;
// do things
return automatic_var;
}
// ...
X obj( foo() );


for which I think the alternative using
std::shared_ptr
, and
std::reference_wrapper
if I get tired of
get()


std::shared_ptr<Y> foo()
{
std::shared_ptr<Y> sptr( new Y() );
// do things
return sptr;
}
// ...
auto sptr = foo();
std::reference_wrapper<Y> ref( *ptr.get() );


is only slightly less clear, but as efficient.

Note 2: I really made an effort to make this question precise and answerable, and not subject to discussion; please think it through and don't interpret it as "Why are move-semantics useful", this is not what I am asking.

Answer

std::shared_ptr stores your data on the free store (runtime overhead), and has a thread safe atomic increment/decrement (runtime overhead), and is nullable (either ignore it and get bugs, or check it constantly for runtime and programmer time overhead), and has a non-trivial to predict lifetime of the object (programmer overhead).

It is not in any way, shape or form as cheap as a move.

Move occurs when NRVO and other forms of elision fail, so if you have a cheap move using objects as values means you can rely on elision. Without cheap move, relying on elision is dangerous: elision is both fragile in practice and not guaranteed by the standard.

Having efficient move also makes containers of objects efficient without having to store containers of smart pointers.

A unique pointer solves some of the problems with shared pointer, except forced free store and nullability, and it also blocks easy use of copy construction.

As an aside, there are issues with your planned move-capable pattern.

First, you needlessly default construct before move constructing. Sometimes the default construct is not free.

Second operator=(X) does not play nice with some defects in the standard. I forget why -- composition or inheritance issue? -- I will try to remember to come back and edit it in.

If default construct is nearly free, and swapping is element-wise, here is a C++14 approach:

struct X{
  auto as_tie(){
    return std::tie( /* data fields of X here with commas */ );
  }
  friend void swap(X& lhs, X& rhs){
    std::swap(lhs.as_tie(), rhs.as_tie());
  }
  X();// implement
  X(X const&o):X(){
    as_tie()=o.as_tie();
  }
  X(X&&o):X(){
    as_tie()=std::move(o.as_tie());
  }
  X&operator=(X&&o)&{// note: lvalue this only
    X tmp{std::move(o)};
    swap(*this,o);
    return *this;
  }
  X&operator=(X const&o)&{// note: lvalue this only
    X tmp{o};
    swap(*this,o);
    return *this;
  }
};

now if you have components that need manual copying (like a unique_ptr) the above does not work. I'd just write a value_ptr myself (that is told how to copy) to keep those details away data consumers.

The as_tie function also makes == and < (and related) easy to write.

If X() is non-trivial, both X(X&&) and X(X const&) can be written manually and efficiency regained. And as operator=(X&&) is so short, having two of them is not bad.

As an alternative:

X& operator=(X&&o)&{
  as_tie()=std::move(o.as_tie());
  return *this;
}

is another implementation of = that has its pluses (and ditto for const&). It can be more efficient in some cases, but has worse exception safety. It also eliminates the need for swap, but I would leave swap in regardless: element-wise swap is worth it.

Comments