Why Is Copy Constructor Not Being Called in This Code

Why is the copy constructor not called?

Whenever a temporary object is created for the sole purpose of being copied and subsequently destroyed, the compiler is allowed to remove the temporary object entirely and construct the result directly in the recipient (i.e. directly in the object that is supposed to receive the copy). In your case

MyClass MyObj(MyClass(1, 2));

can be transformed into

MyClass MyObj(1, 2);

even if the copy constructor has side-effects.

This process is called elision of copy operation. It is described in 12.8/15 in the language standard.

Why copy constructor is not called here?

It is because copy initialization is used here, not copy constructor, due to the NRVO enabled in your compiler. You should specify

-fno-elide-constructors

flag on gcc

The C++ standard allows an implementation to omit creating a temporary
which is only used to initialize another object of the same type.
Specifying this option disables that optimization, and forces G++ to
call the copy constructor in all cases.

or compile it with cl /Od program.cpp on VS (/O1 and /O2 enables NRVO)

C++ : Avoiding copy with the “return” statement

Why is copy constructor not called

Since copying happens frequently in C++ and since it may be expensive the compiler is allowed to elide certain copy (and move) constructions. This copy elision is allowed even if the elided constructor and/or the destructor has side effects like the output in your program (that is, it isn't really an optimization as the behavior with and without copy elision is different).

There are four basic places where copy elision can be applied according to 12.8 [class.copy] paragraph 31:

  1. In a return statement when directly returning a local variable which has the same type as the return type of the function.
  2. In a throw statement a copy of an automatic variable within the innermost try-block can be elided when the object is thrown.
  3. When a temporary object wasn't bound to a reference copying it can be elided.
  4. When a catch clause catches the object by value and with the same type as the object in the throw statement the copy can be elided.

The exact rules are slightly more complicated but I think this is the gist of it. Given that the rules for copy elision are fairly strict it is easy to suppress copy elision: the easiest way is to use an identity() function:

template <typename T>
T const& identity(T const& object) {
return object;
}
...
X d = identity(returnX(a));

(this version also inhibits move construction, though; deducing the type using a T&& and returning it appropriately should make move construction possible but I'm not quite sure what the return type and the return statement should be).

Why the copy constructor is not called?

Copy/move elision is the only allowed exception to the so-called "as-if" rule, which normally constrains the kinds of transformations (e.g. optimizations) that a compiler is allowed to perform on a program.

The rule is intended to allow compilers to perform any optimization they wish as long as the transformed program would work "as if" it was the original one. However, there is one important exception.

Per paragraph 12.8/31 of the C++11 Standard:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the constructor selected for the copy/move operation and/or the destructor for the object
have side effects
. [...] This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which
may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a
    non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified
    type as the function return type, the copy/move operation can be omitted by constructing
    the automatic object directly into the function’s return value

[...]

  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved
    to a class object with the same cv-unqualified type, the copy/move operation can be omitted by
    constructing the temporary object directly into the target of the omitted copy/move

[...]

In other words, you should never rely on a copy constructor or move constructor being called or not being called in the cases for which the provisions of 12.8/31 apply.

Why is the copy constructor not called when objects are allocated on heap?

Some detailed answer, based on you talking about optimization:

Your have the following code:

list<Guitars *> *guitarList;

void AddGuitar(int serNum, float price)
{
Guitars *x = new Guitars(serNum, price);
// Guitars x(serNum,price);
guitarList->push_back(x);
}

I think your reason to use all of those pointers is to be faster. If a Guitars object would be created on the stack as usual and then be pushed back, that would create a copy, true.

What you could do instead would be to define a move operation for Guitars and move the stack object into what you create in the list, like by calling the move constructor.

But even better would be to use std::list::emplace_back, like this:

list<Guitars> guitarList;

void AddGuitar(int serNum, float price)
{
guitarList.emplace_back(serNum, price);
}

In any case, if you talk about optimality, those pointers are not good. A pointer requires additional space, and every time the data is accessed, the pointer must be dereferenced. Also, as @PaulMcKenzie wrote in the comments, this can block the compiler from optimizing for you.

Also, making a list member itself a pointer, that is going with list<Guitars*>* guitarList; or list<Guitars>* guitarList;, is also not a good idea. The only reason I see is if you want to exchange the lists of two Inventory objects, but in that case, simply call std::swap on the lists.

If you drop the pointers, note how instantly every other code of yours becomes far easier. You don't even have to define your destructor at all.

(As for the actual question you asked, like @Jarod42 already wrote, copying pointers does not copy objects.)

(By the way, if the class Guitars represents a single guitar, then I'd go for the singular, Guitar.)

Edit:

I created a small series of tests with different ways to fill the list, using Guitars mostly unmodified. (I removed the assignments of the non-pointers to NULL though.) In any way, I did the following test setup:

#include <iostream>
#include <list>

class Guitar
{
private:
int serialNumber{0};
float price{0.0};

public:
Guitar(int serNum, float price)
{
std::cout << "Base" << std::endl;
this->serialNumber = serNum;
this->price = price;
}
Guitar(const Guitar& s)
: serialNumber{s.serialNumber}, price{s.price}
{
std::cout << "Copy" << std::endl;
}

Guitar(Guitar&& source) noexcept : serialNumber{source.serialNumber}, price{source.price}
{
std::cout << "Move" << std::endl;
}
};

void test_1()
{
std::cout << "test 1" << std::endl;
std::list<Guitar*> guitarList;
Guitar* x = new Guitar(1, 2.);
guitarList.push_back(x);
std::cout << std::endl;
}

void test_2()
{
std::cout << "test 2" << std::endl;
std::list<Guitar> guitarList;
Guitar x(1, 2.);
guitarList.push_back(x);
std::cout << std::endl;
}

void test_3()
{
std::cout << "test 3" << std::endl;
std::list<Guitar> guitarList;
guitarList.push_back(Guitar(1, 2.));
std::cout << std::endl;
}

void test_4()
{
std::cout << "test 4" << std::endl;
std::list<Guitar> guitarList;
guitarList.emplace_back(1, 2.);
std::cout << std::endl;
}

int main()
{
test_1();
test_2();
test_3();
test_4();
}

The output of this is:

test 1
Base

test 2
Base
Copy

test 3
Base
Move

test 4
Base

I hope this increases further understanding about how things work here.

The tests can be found under http://www.cpp.sh/35ld6

Also, I wanted to mention, if we talk about optimization, we'd have to talk about what we optimize. Right now, we have small lists of objects with almost no content. In that case, one would not optimize at all, as we talk about nanoseconds in difference.

The cases to think about are:

  1. A small list of big objects that are easy to move. In that case, we need to make sure that no copy constructor is called, but move would be fine.
  2. A small list of big objects that are hard to move. In that case, we only want to use the base operator, possibly by pointers as you initially did - but emplace_back also works and makes things easier. Note that the objects being hard to move would hint at a bad design for the class.
  3. A big list of small objects. Here we want to use as few constructors as possible, including move constructors. We also don't want to use a list of pointers, as that would give us additional 64 bits per object, and a lot of derefencing later on. In that case, emplace_back really shines.

So in other words, you can't go wrong with emplace_back.

Why is the copy constructor not called when the function returns?

With the above set-up it is quite likely that the compiler elides the copy constructor and, instead, directly constructs the temporary temp in the location where the return value will be expected. Copy elision is explicitly allowed even if the copy constructor has side effects. However, even if the copy is elided, the copy or move constructor still has to be accessible, i.e., the potential of copy elision doesn't relax the rules on the corresponding constructor to be accessible.

If you feel you absolutely want to have a copy constructor called, you can force copy construction, e.g., by passing the result through an identity function:

template <typename T>
T const& identity(T const& object) {
return object;
}
// ...
return identity(temp);

Normally, you'd want the copy constructor to be elided, however.

Copy constructor not called?

Because of Return Value Optimization the 3rd test case's copy constructor call is optimized away by the compiler.



Related Topics



Leave a reply



Submit