4

When I write classes that hold resources, I'm very accustomed to writing simple swap functions to simplify the process of transferring/copying resources. The following code is a trivial example:

class MyArray {
    public:
    MyArray(size_t size) :
    _array(size ? new int[size] : nullptr),
    _size(size)
    {
    }

    MyArray(const MyArray & mv) :
    MyArray(mv._size)
    {
        if(mv._size) std::copy(mv._array, mv._array + mv._size, _array);
    }

    static void friend swap(MyArray & mv1, MyArray & mv2) noexcept {
        std::swap(mv1._array, mv2._array);
        std::swap(mv1._size, mv2._size);
    }

    MyArray(MyArray && mv) {
        swap(*this, mv);
    }

    MyArray & operator=(MyArray mv) {
        swap(*this, mv);
        return *this;
    }

    ~MyArray() noexcept {
        delete[] _array;
    }

    int & operator[](size_t index) {
        if(index >= _size) throw std::exception("Index Out of Bounds!");
        return _array[index];
    }

    const int & operator[](size_t index) const {
        if(index >= _size) throw std::exception("Index Out of Bounds!");
        return _array[index];
    }

    size_t size() const {
        return _size;
    }
private:
    int * _array;
    size_t _size;
};

However, I could have chosen to [equivalently] implement the same code like so:

class MyArray {
    public:
    MyArray(size_t size) :
    _array(size)
    {
    }

    int & operator[](size_t size) {
        if(index >= size()) throw std::exception("Index Out of Bounds!");
        return _array[index];
    }

    const int & operator[](size_t index) const {
        if(index >= size()) throw std::exception("Index Out of Bounds!");
        return _array[index];
    }

    size_t size() const {
        return _array.size();
    }

private:
    std::vector<int> _array;
};

And in the second code, the swap functionality defaults to the normal std::swap behavior (i.e. move-construct the first into the temporary, move-assign the second into the first, and move-assign the temporary into the second), which should be identical (on a design level) to the first code.

The question I have then, is what would be a scenario where I must explicitly define the swap function, but isn't specifically a scenario where I'm just trying to reuse code (like from the first example code)? Are there obvious (or not-so-obvious) scenarios where, even though nothing in my code is explicitly managing a resource, I will objectively benefit from a swap function? Should I just auto-write a swap function for every [move-constructable] class I write, or will I just waste code by doing that?

Xirema
  • 19,889
  • 4
  • 32
  • 68
  • You do not have to define your move constructor in terms of swap; in fact, for const fields, this is not even possible. You might just write your own. – lorro Jun 20 '16 at 19:47
  • @lorro I don't think I implied that I *had* to, but that I *chose* to do so because it made the code simpler. – Xirema Jun 20 '16 at 19:54
  • 1
    `MyArray(MyArray && mv)` is not initializing its members before swapping them into `mv`, thus leaving `mv` in a invalid state, violating move semantics. You need to initialize yourself to a valid blank state first, then swap so `mv` will be set to a blank state, eg: `MyArray(MyArray && mv) : MyArray(0) { swap(*this, mv); }` – Remy Lebeau Jun 21 '16 at 00:36
  • In your second case, the code can be a little simpler if you get rid of the manual bounds checking and use `vector::at()` instead, which does bounds checking for you. – Remy Lebeau Jun 21 '16 at 00:40
  • @RemyLebeau In principle, you're correct. However, in c++, members that don't get explicitly initialized will get implicitly default-initialized, and in this situation, that has the exact same effect as calling `MyArray(0)`. – Xirema Jun 21 '16 at 00:40
  • @Xirema: "*in c++, members that don't get explicitly initialized will get implicitly default-initialized*" - AFAIK, only if a default constructor is not defined manually, or non-POD members have their own default constructors. A compiler-generated default constructor may default-init members, even POD types. But once a non-generated default constructor is defined, it is responsible for initializing POD members, they are not default-init. You can explicitly default-init them in the initialization list. Unless something has changed in C++11 that I am not aware of (as I don't use C++11 yet). – Remy Lebeau Jun 21 '16 at 00:53
  • @RemyLebeau I don't know whether C++11 standardized it or not, but [ideone.com](http://ideone.com/Q7HV8P) seems to feel that default-initialized pointers are set to 0, regardless of whether you use a default-generated constructor or not. – Xirema Jun 21 '16 at 02:15
  • The compilers I use do not default-initialize POD members except when the parent class is instantiated in global memory. POD members have to be explicitly initialized otherwise. Oh well. I don't trust implicit initializations of non-POD types anyway. – Remy Lebeau Jun 21 '16 at 02:25

1 Answers1

5

If you don't define your own swap operation, using std::swap; swap(lhs, rhs); compiles to roughly this:

auto tmp = std::move(rhs);
rhs = std::move(lhs);
lhs = std::move(tmp);

this causes 3 moves to occur, and requires a compete temporary object to exist.

If the move assign and move construct operator has the strong exception guarantee, this swap has a weak exception guarantee. If the move assign and move construct operator is nothrow, so is this swap.

In comparison, member-wise swap can leave objects in incoherent states. If your code can handle this, it means you never have to create a full temporary object. So if you can handle partially swapped components, and full temporary objects (created from a move) have some cost, then a member-wise swap could be faster.

However, if everything is nothrow (which swap / move usually is), the resulting swap operations are logically equivalent.


Your second MyArray doesn't actually manage resources. That is handled by the std::vector, which did write most of the code in your first one.

The rule of Zero is that classes that are not managing resources do not need to write any of the copy or move assignment or construction, nor the destructor. Such classes probably do not need swap either.

Classes that do write copy/move and destruction, ones that manage resources, may use swap to do so. Otherwise, std::swap does a decent job.


As an aside, if they do use swap, and the swap is simply memberwise, write this:

friend auto mytie( MyArray& self ) noexcept {
  return std::tie(self.array, self.size);
}

then swap becomes:

friend void swap( MyArray& lhs, MyArray& rhs ) noexcept {
  std::swap( mytie(lhs), mytie(rhs) );
}

which reduces your "repeat ever member name" boilerplate count by 1.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • The implication of your post is that the paragraph in my original text (*"And in the second code, the swap functionality defaults to the normal `std::swap` behavior (i.e. move-construct the first into the temporary, move-assign the second into the first, and move-assign the temporary into the second), which should be identical (on a design level) to the first code."*) is incorrect. Is that the case? – Xirema Jun 21 '16 at 14:58
  • 1
    @Xirema It is *nearly* identical. If everything involved is nothrow and there is no substatial cost to creating a temporary instance of your class, then they are identical. Suppose your class was a `std::array< std::unique_ptr, 1000000 >`: a smart element-wise swap would never create a temporary array of a million elements, while a full-object `std::swap` default swap would. That cost might matter. Similarly, throwing could result in a difference between `std::swap` and element-wise swap. – Yakk - Adam Nevraumont Jun 21 '16 at 15:38