7

When I learned C++ people told me to always implement at least rule of three methods.

Now I'm seeing the new "... = default;" from c++0x on stack overflow, and my question is:

Is there a c++11 standard implementation defined for those methods or is it compiler specific?

plus I would like to have some precisions:

  • What does the implementation looks like in term of code? (if it's generic)
  • Does this have an advantage compared to my example implementation below?
  • If you don't use assignment/copy constructor, what does *... = delete* do precisly, what's the difference with declaring them private? Answer (from @40two)
  • Is the new default= different from the old default implementation?

Disclaimer: when I'll need more advanced features in my methods, for sure I'll implements them myself. But I get used to implement assignment operator and copy constructor even when I never used them, just in order that the compiler don't.


What I used to do: (edited, @DDrmmr swap/move)

//File T.h
class T
{
  public:
    T(void);
    T(const T &other);
    T(const T &&other);
    T &operator=(T other);
    friend void swap(T &first, T &second);
    ~T(void);

  protected:
    int *_param;
};

//File T.cpp
T::T(void) :
  _param(std::null)
{}

T::T(T &other)
  : _param(other._param)
{}

T::T(T &&other)
  : T()
{
  swap(*this, other);
}

T &T::operator=(T other)
{
  swap(*this, other);
  return (*this);
}

friend void swap(T &first, T &second)
{
  using std::swap;

  swap(first._param, second._param);
}

T::~T(void)
{}
Community
  • 1
  • 1
  • Your assignment operator is not exception safe when you have more than one member variable who's assignment operator could throw. – D Drmmr Jun 27 '14 at 06:24
  • For reference, Coplien's form methods seems to be [the widely more known rule of three](https://en.wikipedia.org/wiki/Rule_of_three_%28C%2B%2B_programming%29) (which should be the rule of five in C++11 and later) plus constructor. – Some programmer dude Jun 27 '14 at 06:25
  • Hum never ran into this problem before cause they are usually exception safe but what would you do about that? Just to know –  Jun 27 '14 at 06:25
  • @JoachimPileborg Yes looks like it's the rule of three, but I didnt learned it in english. Edited. –  Jun 27 '14 at 06:26
  • No need to edit, most experienced programmers will recognize it when they see it. – Some programmer dude Jun 27 '14 at 06:28
  • 2
    @Mayerz You can use the [copy and swap idiom](http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom) to implement the assignment operator using the copy constructor while providing the strong exception safety guarantee. – D Drmmr Jun 27 '14 at 06:32
  • 4
    *When I learned C++ people told me to **always** implement at least rule of three methods.* — if I understand you correctly, this is contrary to the rule of three as usually presented. The point of rule of three is that if you need **any one** of the three methods (destructor, copy ctor, assignment operator), you almost certainly also need the other two. It doesn't mean that you should always implement those three. Many well-written classes, such as those whose members are only STL containers and smart pointers, need none of the rule-of-three methods. – user4815162342 Jun 27 '14 at 06:41
  • 1
    `If you don't use assignment/copy constructor, what does *... = delete* do precisly, what's the difference with declaring them private?` http://stackoverflow.com/questions/18847957/delete-modifier-vs-declaring-function-as-private – 101010 Jun 27 '14 at 06:42
  • Yep I know, but it's how I learned, now I realise that sometimes it's unnecessary. they told us that evne if you don't need them you have to declare it to prevent the compiler to do so, is that correct or? –  Jun 27 '14 at 06:43
  • 2
    If you declare them, the compiler won't do it. But the principle is to *let* the compiler define them as often as you can. Design your classes such that the compiler-generated defaults are the correct solution for you. Only if you *need* special handling in one of the functions, implement it. Rule Of Three then states that in such case, you will most likely need special handling in all 3 and should therefore implement all of them. Here's a [good article on the topic](http://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html). – Angew is no longer proud of SO Jun 27 '14 at 06:49
  • @Angew Thanks for the link, and the explanation. What I used to do is implementing them empty to prevent compiler to do so when I don't need them. good/bad? –  Jun 27 '14 at 06:52
  • 1
    It is best to **let** the compiler implement the methods for you. Only in the cases when the compiler cannot do it properly (typicallly when of the class members is a pointer), should you prevent it by declaring and/or defining your own. – user4815162342 Jun 27 '14 at 06:53
  • 1
    @Mayerz That depends. If you want your class non-copyable (and non-movable), by all means do prevent auto-generation of copy functions (in C++11, you can use `= delete` for that). But I'd say that in typical cases, non-copyable non-movable classes are not that common. C++ isn't Java, we generally prefer to hold our objects by value. – Angew is no longer proud of SO Jun 27 '14 at 06:59
  • 1
    Explicitly defaulting functions can be dangerous, since it can make functions available that the compiler would not have provided otherwise. Don't explicitly default everything blindly. -- But if you need them, explicitly defaulted functions can be handy. For example, they can be *trivial* (user-provided implementations cannot). This has influences on the properties of the class type, e.g. whether or not it's a POD. – dyp Jun 27 '14 at 09:35

1 Answers1

4

The default behavior is:

  • Default ctor ( T() ): calls bases def. ctors and members default ctors.
  • Copy ctor ( T(const T&) ): calls bases copy. ctors and members copy ctors.
  • Move ctor ( T(T&&) ): calls bases move. ctors and members move ctors.
  • Assign ( T& operator=(const T&) ): calls bases assign. and members assign.
  • Transfer ( T& operator=(T&&) ): calls bases transfer, and members transfer.
  • Destructor ( ~T() ): calls member destructor, and bases destructor (reverse order).

For built-in types (int etc.)

  • Default ctor: set to 0 if explicitly called
  • Copy ctor: bitwise copy
  • Move ctor: bitwise copy (no change on the source)
  • Assign: bitwise copy
  • Transfer: bitwise copy
  • Destructor: does nothing.

Since pointers are builtin types as well, this apply to int* ( not to what it points to).

Now, if you don't declare anything, your T class will just hold a int* that does not own the pointed int, so a copy of T will just hold a pointer to the same int. This is the same resulting behavior as C++03. Default implemented move for built-in types are copy. For classes are memberwise move (and depends on what members are: just copies for built-ins)

If you have to change this behavior, you have to do it coherently: for example, if you want to "own" what you point to, you need

  • a default ctor initializing to nullptr: this defines an "empty state" we can refer later
  • a creator ctor initializing to a given pointer
  • a copy ctor initializing to a copy of the pointed (this is the real change)
  • a dtor that deletes the pointed
  • an assign that deletes the pointed and receive a new copy of the pointed

.

T::T() :_param() {}
T::T(int* s) :_param(s) {}
T(const T& s) :_param(s._param? new int(*s._param): nullptr) {}
~T() { delete _param; } // will do nothing if _param is nullptr

Let's not define the assign, by now, but concentrate on the move: If you don't declare it, since you declared the copy, it will be deleted: this makes a T object always being copied even if temporary (same behavior as c++03)

But if the source object is temporary, we can create an empty destination and swap them:

T::T(T&& s) :T() { std::swap(_param, s._param); }

This is what is called a move.

Now the assignment: before C++11 T& operator=(const T& s) should check against a self assignment, make the destination empty and receive a copy of the pointed:

T& operator=(const T& s)
{
    if(this == &s) return *this; // we can shortcut
    int* p = new int(s._param); //get the copy ...
    delete _param; //.. and if succeeded (no exception while copying) ...
    _param = p; // ... delete the old and keep the copy
    return *this;
}

With C++11 we can use the parameter passing to generate the copy, thus giving

T& operator=(T s) //note the signature
{ std::swap(_param, s._param); return *this; }

Note that this works also in C++98, but the pass-by copy will not be optimized in pass-by move if s is temporary. This makes such implementation not profitable in C++98 and C++03 but really convenient in C++11.

Note also that there is no need to specialize std::swap for T: std::swap(a,b); will work, being implemented as three moves (not copy)

The practice to implement a swap function derives for the case where T has many members, being swap required in both move and assign. But it can be a regular private member function.

Emilio Garavaglia
  • 20,229
  • 2
  • 46
  • 63
  • *"but the pass-by copy will not be optimized in pass-by move if s is temporary"* If the argument is a temporary, copy elision will probably take place. – dyp Jun 27 '14 at 09:30
  • @dyp: thanks for pointing out the very confusing typo. About copy elision: yes, but in C++03 that's just a compile feature. In C++11 is a spec. requirement. – Emilio Garavaglia Jun 27 '14 at 09:40
  • Yes, *moving* from an rvalue is a requirement for types that support it. However, usual copy elision for temporaries is implemented in all compilers I know even in debug mode, and it's even faster than moving. In C++11, this extends to *move elision* of course. So for most cases, the behaviour of this code (passing an rvalue to a function which takes by value) won't differ between C++03 and C++11. (Unless you take into account conversions.) – dyp Jun 27 '14 at 09:43
  • Thanks for this great answer @EmilioGaravaglia. I just didn't understood this >Note also that there is no need to specialize std::swap for T: std::swap(a,b); will work, **being implemented as three moves (not copy)** –  Jun 27 '14 at 10:28
  • @Mayerts: `std::swap` is implemented (in C++11 version) as `c(std::move(a)); a=std::move(b); b=std::move(c);` if the class is "movable" a and b swap their pointers. If the class is not "movable", than regular copies will result, since `T&&` decays into `T const&`. – Emilio Garavaglia Jun 27 '14 at 16:10