What's the Motivation Behind Having Copy and Direct Initialization Behave Differently

Why is copy initialization the way it is? Why require the copy constructor?

Copy initialization of an object is ambiguous to direct initialization, both can be used to the same extent in order to set values equal to each other.

int a = 4;
int a = int(4);
int a(4);

all of these calls are ambiguous, they all set a equal to 4. The reason for a copy constructor in the case of an integer is convenience, imagine c++ data types without this

int a(foo(b,r)); //a little messy for a variable declaration
int a = foo(b,r) //ok, cleaner

you also might might to use an implicit and explicit copy constructor, here is an example program that uses a copy constructor explicitly to handle imaginary numbers:

#include <iostream>
using std::cout;
using std::endl;
class complexNumbers {
double real, img;
public:
complexNumbers() : real(0), img(0) { }
complexNumbers(const complexNumbers& c) { real = c.real; img = c.img; }
explicit complexNumbers( double r, double i = 0.0) { real = r; img = i; }
friend void display(complexNumbers cx);
};
void display(complexNumbers cx){
cout<<"Real Part: "<<cx.real<<" Imag Part: "<<cx.img<<endl;
}
int main() {
complexNumbers one(1);
display(one);
complexNumbers two =2;
display(200);
return 0;
}

Difference copy-initialization and direct-initialization for primitive (scalar) types

There is no difference for primitive scalars like this; the memory location or register (depending on usage) is going to be initialized the same way.

Is there a difference between copy initialization and direct initialization?

C++17 Update

In C++17, the meaning of A_factory_func() changed from creating a temporary object (C++<=14) to just specifying the initialization of whatever object this expression is initialized to (loosely speaking) in C++17. These objects (called "result objects") are the variables created by a declaration (like a1), artificial objects created when the initialization ends up being discarded, or if an object is needed for reference binding (like, in A_factory_func();. In the last case, an object is artificially created, called "temporary materialization", because A_factory_func() doesn't have a variable or reference that otherwise would require an object to exist).

As examples in our case, in the case of a1 and a2 special rules say that in such declarations, the result object of a prvalue initializer of the same type as a1 is variable a1, and therefore A_factory_func() directly initializes the object a1. Any intermediary functional-style cast would not have any effect, because A_factory_func(another-prvalue) just "passes through" the result object of the outer prvalue to be also the result object of the inner prvalue.


A a1 = A_factory_func();
A a2(A_factory_func());

Depends on what type A_factory_func() returns. I assume it returns an A - then it's doing the same - except that when the copy constructor is explicit, then the first one will fail. Read 8.6/14

double b1 = 0.5;
double b2(0.5);

This is doing the same because it's a built-in type (this means not a class type here). Read 8.6/14.

A c1;
A c2 = A();
A c3(A());

This is not doing the same. The first default-initializes if A is a non-POD, and doesn't do any initialization for a POD (Read 8.6/9). The second copy initializes: Value-initializes a temporary and then copies that value into c2 (Read 5.2.3/2 and 8.6/14). This of course will require a non-explicit copy constructor (Read 8.6/14 and 12.3.1/3 and 13.3.1.3/1 ). The third creates a function declaration for a function c3 that returns an A and that takes a function pointer to a function returning a A (Read 8.2).


Delving into Initializations Direct and Copy initialization

While they look identical and are supposed to do the same, these two forms are remarkably different in certain cases. The two forms of initialization are direct and copy initialization:

T t(x);
T t = x;

There is behavior we can attribute to each of them:

  • Direct initialization behaves like a function call to an overloaded function: The functions, in this case, are the constructors of T (including explicit ones), and the argument is x. Overload resolution will find the best matching constructor, and when needed will do any implicit conversion required.
  • Copy initialization constructs an implicit conversion sequence: It tries to convert x to an object of type T. (It then may copy over that object into the to-initialized object, so a copy constructor is needed too - but this is not important below)

As you see, copy initialization is in some way a part of direct initialization with regard to possible implicit conversions: While direct initialization has all constructors available to call, and in addition can do any implicit conversion it needs to match up argument types, copy initialization can just set up one implicit conversion sequence.

I tried hard and got the following code to output different text for each of those forms, without using the "obvious" through explicit constructors.

#include <iostream>
struct B;
struct A {
operator B();
};

struct B {
B() { }
B(A const&) { std::cout << "<direct> "; }
};

A::operator B() { std::cout << "<copy> "; return B(); }

int main() {
A a;
B b1(a); // 1)
B b2 = a; // 2)
}
// output: <direct> <copy>

How does it work, and why does it output that result?

  1. Direct initialization

    It first doesn't know anything about conversion. It will just try to call a constructor. In this case, the following constructor is available and is an exact match:

    B(A const&)

    There is no conversion, much less a user defined conversion, needed to call that constructor (note that no const qualification conversion happens here either). And so direct initialization will call it.

  2. Copy initialization

    As said above, copy initialization will construct a conversion sequence when a has not type B or derived from it (which is clearly the case here). So it will look for ways to do the conversion, and will find the following candidates

    B(A const&)
    operator B(A&);

    Notice how I rewrote the conversion function: The parameter type reflects the type of the this pointer, which in a non-const member function is to non-const. Now, we call these candidates with x as argument. The winner is the conversion function: Because if we have two candidate functions both accepting a reference to the same type, then the less const version wins (this is, by the way, also the mechanism that prefers non-const member function calls for non-const objects).

    Note that if we change the conversion function to be a const member function, then the conversion is ambiguous (because both have a parameter type of A const& then): The Comeau compiler rejects it properly, but GCC accepts it in non-pedantic mode. Switching to -pedantic makes it output the proper ambiguity warning too, though.

I hope this helps somewhat to make it clearer how these two forms differ!

direct initialization and copy initialization of reference

The main differences between direct-initialization and copy-initialization are covered in section 8.5, paragraph 17 of the standard. In general, the difference is that for class types in copy-initialization, explicit constructors are not considered (only converting constructors are considered) and a possibly elided copy is made; in direct-initialization explicit constructors are considered and the target is constructed directly. From section 8.5 of the standard:

14 - The form of initialization (using parentheses or =) is generally insignificant, but does matter when the initializer or the entity being initialized has a class type [...]

For non-class types (including references), direct-initialization and copy-initialization have similar semantics; for references, a reference binding occurs in either case, as specified in 8.5.3 References [dcl.init.ref]. Direct-initialization and copy-initialization of a reference only have different semantics where a conversion function is involved (13.3.1.6 Initialization by conversion function for direct reference binding [over.match.ref]); again, direct-initialization is allowed to invoke explicit conversion functions where copy-initialization is not.

So, in

int &j = i;

8.5.3p5 applies and the reference j is bound directly to the lvalue i. No temporaries are invoked.

In terms of complexity, references are closer to fundamental (primitive) types than to class types. Primitives are initialized without a temporary being constructed (8.5p17, last bullet) and in general references are too. This is probably why the book only uses the = form for initialization of references; as with primitives, there is usually no difference and writing int i = x; is usually clearer than int i(x);.

c++ - Direct and Copy Constructors

This isn't copy initialization:

UnusualClass k2=56;   // NOT Copy initialization
// for 56 is not of type UnusualClass

It will call the constructor:

UnusualClass(int a)

I think you meant:

UnusualClass k1(5);    //Direct initialization
UnusualClass k2{k1}; //Copy initialization
UnusualClass k2 = k1; //Copy initialization

Note the type needed in copy initialization.

UnusualClass(const UnusualClass &n) // const reference to type UnusualClass

It should be the object's type which is UnusualClass, not int

UPDATE

I get an error saying use of deleted function

UnusualClass::UnusualClass(const UnusualClass&).

Why would I get this error if it skips this constructor anyways?

UnusualClass::UnusualClass(const UnusualClass&) = delete;

means:

From cppreference

Avoiding implicit generation of the copy constructor.

Thus, you will need to define your own copy constructor.

UPDATE 2

Refer more to @songyuanyao's answer for copy-initialization

Strange behavior of copy-initialization, doesn't call the copy-constructor!

Are you asking why the compiler does the access check? 12.8/14 in C++03:

A program is ill-formed if the copy
constructor or the copy assignment
operator for an object is implicitly
used and the special member function
is not accessible

When the implementation "omits the copy construction" (permitted by 12.8/15), I don't believe this means that the copy ctor is no longer "implicitly used", it just isn't executed.

Or are you asking why the standard says that? If copy elision were an exception to this rule about the access check, your program would be well-formed in implementations that successfully perform the elision, but ill-formed in implementations that don't.

I'm pretty sure the authors would consider this a Bad Thing. Certainly it's easier to write portable code this way -- the compiler tells you if you write code that attempts to copy a non-copyable object, even if the copy happens to be elided in your implementation. I suspect that it could also inconvenience implementers to figure out whether the optimization will be successful before checking access (or to defer the access check until after the optimization is attempted), although I have no idea whether that warranted consideration.

Could this behavior be dangerous? I
mean, I might do some other useful
thing in the copy-ctor, but if it
doesn't call it, then does it not
alter the behavior of the program?

Of course it could be dangerous - side-effects in copy constructors occur if and only if the object is actually copied, and you should design them accordingly: the standard says copies can be elided, so don't put code in a copy constructor unless you're happy for it to be elided under the conditions defined in 12.8/15:

MyObject(const MyObject &other) {
std::cout << "copy " << (void*)(&other) << " to " << (void*)this << "\n"; // OK
std::cout << "object returned from function\n"; // dangerous: if the copy is
// elided then an object will be returned but you won't see the message.
}

Why is copy constructor called instead of conversion constructor?

B b2 = a;

This is known as Copy Initialization.

It does the following:

  1. Create an object of type B from a by using B (const A& a).
  2. Copy the created temporary object to b2 by using B (const B& b).
  3. Destroy the temporary object by using ~B().

The error you get is not at step 1 but rather at step 2.

Where is this in the standard?

C++03 8.5 Initializers
Para 14:

....

— If the destination type is a (possibly cv-qualified) class type:

...

...

— Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the destination type. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization. In certain cases, an implementation is permitted to eliminate the copying inherent in this direct-initialization by constructing the intermediate result directly into the object being initialized; see 12.2, 12.8.

Copy initialization with conversion operator

This is very simple: implicit convertibility (for user-defined types) is not a transitive property in C++. That is, while A is convertible to int, and int is convertible to B, this does not by itself mean that A is convertible to B. There is no valid implicit conversion sequence from A to B, so A is not convertible to B.

Copy initialization requires implicit convertibility between the type of the initialization expression and the type being initialized. Since A is not convertible to B, b1 doesn't compile.

Anytime you see {} as part of an initialization like this, then this means that the object it applies to is going to undergo some form of list initialization, which is a different process from the other forms of initialization.

b2 does not perform "direct initialization". It performs direct list initialization, which is one of two forms of list initialization. The rules of list initialization, in this case, eventually boil down to using overload resolution on the set of constructors for B. There is a constructor of B which can be called with an A, since that call can happen via implicit conversion of the A to an int.

b3 is not "copy initialization"; it is copy list initialization, which has absolutely nothing to do with "copy initialization". Copy-list-initialization is identical to direct-list-initialization with exactly two exceptions, neither of which apply to this case. So it does the same thing as b2.



Related Topics



Leave a reply



Submit