The following "minimal" example should show the use of rule of 3 (and a half).
#include <algorithm>
#include <iostream>
class C
{
std::string* str;
public:
C()
: str(new std::string("default constructed"))
{
std::cout << "std ctor called" << std::endl;
}
C(std::string* _str)
: str(_str)
{
std::cout << "string ctor called, "
<< "address:" << str << std::endl;
}
// copy ctor: does a hard copy of the string
C(const C& other)
: str(new std::string(*(other.str)))
{
std::cout << "copy ctor called" << std::endl;
}
friend void swap(C& c1, C& c2) {
using std::swap;
swap(c1.str, c2.str);
}
const C& operator=(C src) // rule of 3.5
{
using std::swap;
swap(*this, src);
std::cout << "operator= called" << std::endl;
return *this;
}
C get_new() {
return C(str);
}
void print_address() { std::cout << str << std::endl; }
};
int main()
{
C a, b;
a = b.get_new();
a.print_address();
return 0;
}
Compiled it like this (g++ version: 4.7.1):
g++ -Wall test.cpp -o test
Now, what should happen? I assumed that the line a = b.get_new();
would make a hard copy, i.e. allocate a new string. Reason: The operator=()
takes its argument, as typical in this design pattern, per value, which invokes a copy ctor, which will make a deep copy. What really happened?
std ctor called
std ctor called
string ctor called, address:0x433d0b0
operator= called
0x433d0b0
The copy ctor was never being called, and thus, the copy was soft - both pointers were equal. Why is the copy ctor not being called?
The rule of three (also known as the law of the big three or the big three) is a rule of thumb in C++ (prior to C++11) that claims that if a class defines any of the following then it should probably explicitly define all three: destructor. copy constructor. copy assignment operator.
In that case, remember the rule of three: If you need to explicitly declare either the destructor, copy constructor or copy assignment operator yourself, you probably need to explicitly declare all three of them.
The copy-and-swap idiom provides an elegant technique to avoid these problems. It utilizes the copy constructor to create a temporary object, and exchanges its contents with itself using a non-throwing swap. Therefore, it swaps the old data with new data. The temporary object is then destructed automatically (RAII).
In C++ we often associate three language features with copy control: destructors, copy constructors, and assignment operators. We will call these the Big 3 because often times when you need to write any one of them, you most likely will need to write the other two.
The copies are being elided.
There's no copy because b.get_new();
is constructing its 'temporary' C
object exactly in the location that ends up being the parameter for operator=
. The compiler is able to manage this because everything is in a single translation unit so it has sufficient information to do such transformations.
You can eliminate construction elision in clang and gcc with the flag -fno-elide-constructors
, and then the output will be like:
std ctor called
std ctor called
string ctor called, address:0x1b42070
copy ctor called
copy ctor called
operator= called
0x1b420f0
The first copy is eliminated by the Return Value Optimization. With RVO the function constructs the object that is eventually returned directly into the location where the return value should go.
I'm not sure that there's a special name for elision of the second copy. That's the copy from the return value of get_new()
into the parameter for operator= ()
.
As I said before, eliding both copies together results in get_new()
constructing its object directly into the space for the parameter to operator= ()
.
Note that both pointers being equal, as in:
std ctor called
std ctor called
string ctor called, address:0xc340d0
operator= called
0xc340d0
does not itself indicate an error, and this will not cause a double free; Because the copy was elided, there isn't an additional copy of that object retaining ownership over the allocated string, so there won't be an additional free.
However your code does contain an error unrelated to the rule of three: get_new()
is passing a pointer to the object's own str
member, and the explicit object it creates (at the line "string ctor called, address:0xc340d0" in the output) is taking ownership of the str
object already managed by the original object (b
). This means that b
and the object created inside get_new()
are both attempting to manage the same string and that will result in a double free (if the destructor were implemented).
To see this change the default constructor to display the str
it creates:
C()
: str(new std::string("default constructed"))
{
std::cout << "std ctor called. Address: " << str << std::endl;
}
And now the output will be like:
std ctor called. Address: 0x1cdf010
std ctor called. Address: 0x1cdf070
string ctor called, address:0x1cdf070
operator= called
0x1cdf070
So there's no problem with the last two pointers printed being the same. The problem is with the second and third pointers being printed. Fixing get_new()
:
C get_new() {
return C(new std::string(*str));
}
changes the output to:
std ctor called. Address: 0xec3010
std ctor called. Address: 0xec3070
string ctor called, address:0xec30d0
operator= called
0xec30d0
and solves any potential problem with double frees.
C++ is allowed to optimize away copy construction in functions that are returning a class instance.
What happens in get_new
is that the object freshly constructed from _str
member is returned directly and it's then used as the source for the assignment. This is called "Return Value Optimization" (RVO).
Note that while the compiler is free to optimize away a copy construction still it's required to check that copy construction can be legally called. If for example instead of a member function you have a non-friend function returning and instance and the copy constructor is private then you would get a compiler error even if after making the function accessible the copy could end up optimized away.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With