3

Consider the following code:

#include<iostream>
using namespace std;
class A{
public:
  A()=default;
  A(int a){cout<<"A(int) called"<<endl;}
  A(const A&a);
  ~A(){cout<<"~A() called"<<endl;}
};
A::A(const A&a){
  cout<<"A(const A&) called"<<endl;
}
int main(){
   A a = 1;
}

When I use g++8.1 to compile with -fno-elide-constructors to cancel the RVO, the output was:

A(int) called
A(const A&) called
~A() called
~A() called

I know that this is something called the converting constructor, an implicit conversion from the types of its arguments to the type of its class.

It seems that firstly a temporary object is constructed by A(int), secondly the a object is constructed by the copy-constructor A(const A&).

But when I modify my code:

#include<iostream>
using namespace std;
class A{
public:
  A()=default;
  A(int a){cout<<"A(int) called"<<endl;}
  explicit A(const A&a); //use explicit
  ~A(){cout<<"~A() called"<<endl;}
};
A::A(const A&a){
  cout<<"A(const A&) called"<<endl;
}
int main(){
   //A a = A(1); //error:no constructor match
   A b = 1; //ok
}

It confused me that the b object is copy-constructed explicitly?! Even if I use the copy initialization?
When I delete the copy-constructor by A(const A&a)=delete;, it will not work as expected.

However, it is different when I use VS2017. the implicit conversion A a = 1; has nothing to do with the copy-constructor. Even if I delete the c-c, it works as usual.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
sakugawa
  • 117
  • 5

2 Answers2

1

The effect of copy initialization here is,

(emphasis mine)

If T is a class type, and the cv-unqualified version of the type of other is not T or derived from T, or if T is non-class type, but the type of other is a class type, user-defined conversion sequences that can convert from the type of other to T (or to a type derived from T if T is a class type and a conversion function is available) are examined and the best one is selected through overload resolution. The result of the conversion, which is a prvalue temporary (until C++17) prvalue expression (since C++17) if a converting constructor was used, is then used to direct-initialize the object. The last step is usually optimized out and the result of the conversion is constructed directly in the memory allocated for the target object, but the appropriate constructor (move or copy) is required to be accessible even though it's not used. (until C++17)

Note that the ojbect is direct-initialized from the converted A (from int), the copy constructor is marked as explicit or not doesn't matter here.


BTW: Since C++17 because of mandatory copy elision, the copy-construction will be elided completely.

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • Does it mean that when I use converting construction like `A a = 1;`, it directly acts like `A a(A(1))`? – sakugawa Jul 21 '20 at 07:51
  • @sakugawa Yes, in concept. Note that for constructing `A(1)` the converting constructor has to be non-explicit. – songyuanyao Jul 21 '20 at 07:53
  • But why the converting construction has nothing to do with the copy-constructor in VS2017 with stdc++14? I set to `=delete` but still works. – sakugawa Jul 21 '20 at 08:16
  • @sakugawa I'm not sure, MSVS might perform mandatory copy elision even in pre-C++17 mode, or it has bug in processing copy elision. In pre-C++17, even copy elision is performed the copy/move constructor need to be present and accessible. – songyuanyao Jul 21 '20 at 08:26
  • I use g++8.1 with stdc++17 it does perform the copy elision as you said while c++11 and c++14 both didn't. It may have bug on MSVC with stdc++14. – sakugawa Jul 21 '20 at 08:56
1

The difference between

A a = A(1);

and

A b = 1;

is that in the second case, the effect that applies is that one described in @songyuanyao's answer. But, the effect in the first case is another one:

If T is a class type and the cv-unqualified version of the type of other is T or a class derived from T, the non-explicit constructors of T are examined and the best match is selected by overload resolution. The constructor is then called to initialize the object.

That's why it does not work with explicit copy constructor.

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93