12

I saw many code implementing rule of five in terms of copy and swap, but I think we can use a move function to replace the swap function as in the following code:

#include <algorithm>
#include <cstddef>

class DumbArray {
public:
    DumbArray(std::size_t size = 0)
        : size_(size), array_(size_ ? new int[size_]() : nullptr) {
    }

    DumbArray(const DumbArray& that)
        : size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
        std::copy(that.array_, that.array_ + size_, array_);
    }

    DumbArray(DumbArray&& that) : DumbArray() {
        move_to_this(that);
    }

    ~DumbArray() {
        delete [] array_;
    }

    DumbArray& operator=(DumbArray that) {
        move_to_this(that);
        return *this;
    }

private:
    void move_to_this(DumbArray &that) {
        delete [] array_;
        array_ = that.array_;
        size_ = that.size_;
        that.array_ = nullptr;
        that.size_ = 0;
   }

private:
    std::size_t size_;
    int* array_;
};

This code, I think

  1. Exception safe
  2. Require less typing, as many function just call move_to_this(), and copy assignment and move assignment are unified in one single function
  3. More efficient than copy-and-swap, as swap involves 3 assignments, while here just 2, and this code doesn't suffer the problems mentioned in This Link

Am I right?

Thanks

Edit:

  1. As @Leon pointed out, maybe a dedicated function for freeing resource is needed, to avoid code duplication in move_to_this() and destructor
  2. As @thorsan pointed out, for extreme performance concern, it's better to seperate DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; } into DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); return *this; }(thanks to @MikeMB) and DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; } to avoid an extra move operatoin

    After adding some debug print, I found that no extra move is involved in DumbArray& operator=(DumbArray that) {} when you call it as a move assignment

  3. As @Erik Alapää pointed out, a self-assignment check is needed before delete in move_to_this()

Chen Rushan
  • 473
  • 4
  • 10
  • 10
    more suitable for [codereview.se]? – cpplearner Jun 20 '16 at 05:26
  • 1
    The code is not exception safe because at least the new and delete can throw. – Resurrection Jun 20 '16 at 05:28
  • 1
    @Resurrection, what I mean by "exception safe" is not that it doesn't throw exception, but that even if an exception is thrown, it doesn't break anything and make the existing object invalid – Chen Rushan Jun 20 '16 at 05:33
  • 3
    Off topic maybe but you havent implemented the five, the assignment operator you have implemented is neither copy nor move, copy: `DumbArray& operator=(DumbArray& that)` and move `DumbArray& operator=(DumbArray&& that)`. And copy and move cannot be the same, thus move_to_this cannot be used for copy and move. – thorsan Jun 20 '16 at 05:46
  • 1
    Is there any plan that the element type will be changed from `int` to some non-trivial class type? –  Jun 20 '16 at 05:51
  • 1
    @thorsan: In fact copy and move assignment can be handled by the same functiion. The difference lies in how the parameter is constructed (either via move ore copy construction). – MikeMB Jun 20 '16 at 05:54
  • @MikeMB, but if you move when you copy, wouldnt that break,unless documented, the expectations of the programmer using the code? the expectation being that the object which is being copied from are not valid after copy? And copying from const objects would have to behave different than when copying from non-const objects. That would be confusing, but offcourse, you could do it. – thorsan Jun 20 '16 at 05:57
  • @Thorsan: That is the point, If you get an lvalue as an argument, you are first copying and then moving from your internal copy. If you get an r-value you are moving two times. The compiler automatically chooses which constructor to invoke. – MikeMB Jun 20 '16 at 06:00
  • @thorsan, `DumbArray& operator=(DumbArray that)` equals `DumbArray& operator=(DumbArray& that)` and `DumbArray& operator=(DumbArray&& that)` combined. If you pass in a rvalue, it will be directly constructed in `that`, and if you pass in a lvalue reference or lvalue, it will be copied into `that`, there is no performance issue here, as no mater what, `that` should be copied in copy constructor – Chen Rushan Jun 20 '16 at 06:05
  • Closely related: http://stackoverflow.com/questions/24014130/should-the-copy-and-swap-idiom-become-the-copy-and-move-idiom-in-c11 – MikeMB Jun 20 '16 at 06:05
  • @NickyC, this is just a toy program, I just want to illustrate that maybe copy and swap should be implemented this way, which is more clear, and I think if you move to a more complex type than `int`, it's easy to modify `move_to_this` for that type – Chen Rushan Jun 20 '16 at 06:08
  • @ChenRushan: But then you get an extra move before move assignment. Moving is for improving performance over doing copying, but this code does two moves for move assignment, why implement like it like this? – thorsan Jun 20 '16 at 06:10
  • @thorsan, no extra copy actually, you can figure it out by enumerating all possible calls to `DumbArray& operator=(DumbArray that)` – Chen Rushan Jun 20 '16 at 06:14
  • I meant extra move, sorry, @ChenRushan – thorsan Jun 20 '16 at 06:14
  • @thorsan, for extra move, you're right. My major concern is the comparison to copy-and-swap, even if you seperate `DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; }` into `DumbArray& operator=(DumbArray &that) { move_to_this(that); return *this; }` and `DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; }`, I think the above implementation is still better than traditional copy-and-swap implementation – Chen Rushan Jun 20 '16 at 06:19
  • @ChenRushan I just gave the code a quick read, but it seems you need to check for self-assignment before deleting. – Erik Alapää Jun 20 '16 at 06:37
  • @ErikAlapää, you're right – Chen Rushan Jun 20 '16 at 06:39
  • @Erik: The assignment operator takes care of that by making a copy. Is it worth protecting the move constructor against that? –  Jun 20 '16 at 06:59
  • @Chen: Beg your pardon, but the copy assignment you show in your last comment is wrong (it has the wrong signature and is missing the copy) – MikeMB Jun 20 '16 at 07:01
  • Missing `noexcept`. I've seen code written in this way before, I don't see anything particularly wrong with it. – Marc Glisse Jun 20 '16 at 07:08
  • @Hurkyl That is an interesting discussion in itself. Here is one link: http://stackoverflow.com/questions/9322174/move-assignment-operator-and-if-this-rhs – Erik Alapää Jun 20 '16 at 07:23
  • @MikeMB, should be `DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); return *this; }`, right? – Chen Rushan Jun 20 '16 at 07:40
  • Beware of self-assignment! – Benoit Jun 20 '16 at 11:09
  • @thorsan, I added some debug output, and found no extra move is involved when you call `DumbArray& operator=(DumbArray that)` as a move assignment, `that` will directly point to the rvalue – Chen Rushan Jun 21 '16 at 13:00

2 Answers2

10

comments inline, but briefly:

  • you want all move assignments and move constructors to be noexcept if at all possible. The standard library is much faster if you enable this, because it can elide any exception handling from algorithms which reorder a sequence of your object.

  • if you're going to define a custom destructor, make it noexcept. Why open pandora's box? I was wrong about this. It's noexcept by default.

  • In this case, providing the strong exception guarantee is painless and costs almost nothing, so let's do that.

code:

#include <algorithm>
#include <cstddef>

class DumbArray {
public:
    DumbArray(std::size_t size = 0)
    : size_(size), array_(size_ ? new int[size_]() : nullptr) {
    }

    DumbArray(const DumbArray& that)
    : size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
        std::copy(that.array_, that.array_ + size_, array_);
    }

    // the move constructor becomes the heart of all move operations.
    // note that it is noexcept - this means our object will behave well
    // when contained by a std:: container
    DumbArray(DumbArray&& that) noexcept
    : size_(that.size_)
    , array_(that.array_)
    {
        that.size_ = 0;
        that.array_ = nullptr;
    }

    // noexcept, otherwise all kinds of nasty things can happen
    ~DumbArray() // noexcept - this is implied.
    {
        delete [] array_;
    }

    // I see that you were doing by re-using the assignment operator
    // for copy-assignment and move-assignment but unfortunately
    // that was preventing us from making the move-assignment operator
    // noexcept (see later)
    DumbArray& operator=(const DumbArray& that)
    {
        // copy-swap idiom provides strong exception guarantee for no cost
        DumbArray(that).swap(*this);
        return *this;
    }

    // move-assignment is now noexcept (because move-constructor is noexcept
    // and swap is noexcept) This makes vector manipulations of DumbArray
    // many orders of magnitude faster than they would otherwise be
    // (e.g. insert, partition, sort, etc)
    DumbArray& operator=(DumbArray&& that) noexcept {
        DumbArray(std::move(that)).swap(*this);
        return *this;
    }


    // provide a noexcept swap. It's the heart of all move and copy ops
    // and again, providing it helps std containers and algorithms 
    // to be efficient. Standard idioms exist because they work.
    void swap(DumbArray& that) noexcept {
        std::swap(size_, that.size_);
        std::swap(array_, that.array_);
    }

private:
    std::size_t size_;
    int* array_;
};

There is one further performance improvement one could make in the move-assignment operator.

The solution I have offered provides the guarantee that a moved-from array will be empty (with resources deallocated). This may not be what you want. For example if you tracked the capacity and the size of a DumbArray separately (for example, like std::vector), then you may well want any allocated memory in this to be retained in that after the move. This would then allow that to be assigned to while possibly getting away without another memory allocation.

To enable this optimisation, we simply implement the move-assign operator in terms of (noexcept) swap:

so from this:

    /// @pre that must be in a valid state
    /// @post that is guaranteed to be empty() and not allocated()
    ///
    DumbArray& operator=(DumbArray&& that) noexcept {
        DumbArray(std::move(that)).swap(*this);
        return *this;
    }

to this:

    /// @pre that must be in a valid state
    /// @post that will be in an undefined but valid state
    DumbArray& operator=(DumbArray&& that) noexcept {
        swap(that);
        return *this;
    }

In the case of the DumbArray, it's probably worth using the more relaxed form in practice, but beware of subtle bugs.

e.g.

DumbArray x = { .... };
do_something(std::move(x));

// here: we will get a segfault if we implement the fully destructive
// variant. The optimised variant *may* not crash, it may just do
// something_else with some previously-used data.
// depending on your application, this may be a security risk 

something_else(x);   
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 2
    Aren't destructors `noexcept (true)` by default now? – JDługosz Jun 20 '16 at 08:49
  • @JDługosz only if defaulted IIRC – Richard Hodges Jun 20 '16 at 09:05
  • @RichardHodges [C++11 Working Draft N3376](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3376.pdf) seems to disagree with you (12.4 Destructors, item 3): `A declaration of a destructor that does not have an exception-specification is implicitly considered to have the same exception-specification as an implicit declaration`. – Alexander Revo Jun 20 '16 at 09:57
1

The only (small) problem with your code is the duplication of functionality between move_to_this() and the destructor, which is a maintenance issue should your class need to be changed. Of course it can be solved by extracting that part into a common function destroy().

My critique of the "problems" discussed by Scott Meyers in his blog post:

He tries to manually optimize where the compiler could do an equally good job if it is smart enough. The rule-of-five can be reduced to the rule-of-four by

  • providing only the copy assignment operator that takes its argument by value and
  • not bothering to write the move assignment operator (exactly what you did).

This automatically solves the problem of the resources of the left-hand-side object being swapped into the right-hand-side object and not being immediately released if the right-hand-side object is not a temporary.

Then, inside the implementation of the copy assignment operator according to the copy-and-swap idiom, swap() will take as one of its arguments an expiring object. If the compiler can inline the destructor of the latter, then it will definitely eliminate the extra pointer assignment - indeed, why save the pointer that is going to be deleteed on the next step?

My conclusion is that it is simpler to follow the well established idiom instead of slightly complicating the implementation for the sake of micro-optimizations that are well within the reach of a mature compiler.

Leon
  • 31,443
  • 4
  • 72
  • 97
  • For your first point, I don't think so, you may miss the `: DumbArray()` part of move constructor – Chen Rushan Jun 20 '16 at 06:11
  • I think the duplication can be fixed by creating another function dedicated to freeing resource which are called by both `move_to_this()` and destructor – Chen Rushan Jun 20 '16 at 06:25
  • I'm not really convinced that - in practice - copy-and-move is more complicated than copy-and-swap, but my general position is to throw the copy-and-swap idiom as well as the rule of 0,1,2,3,4,5... out of the window anyway - at least as a default guideline. It was a (relatively) simple and clear cut guideline in c++03 but in modern c++11-and-beyond code you can't really avoid making a per-class decision about each member function anyway (most of the time, the defaults will do). But that discussion is probably of topic here. – MikeMB Jun 20 '16 at 08:27