Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In C++'s rule of three, why does operator= not call copy ctor?

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?

like image 823
Johannes Avatar asked Oct 11 '13 19:10

Johannes


People also ask

What is the rule of 3 How has that rule changed in C ++ 11?

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.

What is the rule of three stack overflow?

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.

What is copy and swap idiom?

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).

Why do we need the Big 3 in C++?

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.


2 Answers

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.

like image 98
bames53 Avatar answered Nov 15 '22 05:11

bames53


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.

like image 35
6502 Avatar answered Nov 15 '22 05:11

6502