views:

977

answers:

5

Suppose I have this function:

void my_test()
{
    A a1 = A_factory_func();
    A a2(A_factory_func());

    double b1 = 0.5;
    double b2(0.5);

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

In each grouping, are these statements identical? Or is there an extra (possible optimizable) copy in some of the initializations? I have seen people say both things. Please cite text as proof. Also add other cases please.

+9  A: 

Assignment is different from initialization.

Both of the following lines do initialization. A single constructor call is done:

A a1 = A_factory_func();  // calls copy constructor
A a1(A_factory_func());   // calls copy constructor

but it's not equivalent to:

A a1;                     // calls default constructor
a1 = A_factory_func();    // (assignment) calls operator =

I don't have a text at the moment to prove this but it's very easy to experiment:

#include <iostream>
using namespace std;

class A {
public:
    A() { 
     cout << "default constructor" << endl;
    }

    A(const A& x) { 
     cout << "copy constructor" << endl;
    }

    const A& operator = (const A& x) {
     cout << "operator =" << endl;
     return *this;
    }
};

int main() {
    A a;       // default constructor
    A b(a);    // copy constructor
    A c = a;   // copy constructor
    c = b;     // operator =
    return 0;
}
Mehrdad Afshari
Good reference: "The C++ Programming Language, Special Edition" by Bjarne Stroustrup, section 10.4.4.1 (page 245). Describes copy initialization and copy assignment and why they are fundamentally different (though they both use the = operator as syntax).
Naaff
Minor nit, but I really don't like when people say that "A a( x )" and "A a = x" are equal. Strictly they're not. In lots of cases they will do exactly the same thing but it is possible to create examples where depending on the argument different constructors are actually called.
Richard Corden
I'm not talking about "syntactic equivalence." Semantically, both ways of *initialization* are the same.
Mehrdad Afshari
A: 

A lot of these cases are subject to an object's implementation so it's hard to give you a concrete answer.

Consider the case

A a = 5;
A a(5);

In this case assuming a proper assignment operator & initializing constructor which accept a single integer argument, how I implement said methods affects the behavior of each line. It is common practice however for one of those to call the other in the implementation as to eliminate duplicate code (although in a case as simple as this there would be no real purpose.)

Edit: As mentioned in other responses, the first line will in fact call the copy constructor. Consider the comments relating to the assignment operator as behavior pertaining to a stand alone assignment.

That said, how the compiler optimizes the code will then have it's own impact. If I have the initializing constructor calling the "=" operator - if the compiler makes no optimizations, the top line would then perform 2 jumps as opposed to one in the bottom line.

Now, for the most common situations, your compiler will optimize through these cases and eliminate this type of inefficiencies. So effectively all the different situations you describe will turn out the same. If you want to see exactly what is being done, you can look at the object code or an assembly output of your compiler.

dborba
It's not an *optimization*. The compiler *has to* call the constructor alike in both cases. As a result, none of them will compile if you just have `operator =(const int)` and no `A(const int)`. See @jia3ep's answer for more details.
Mehrdad Afshari
I believe you're correct actually. However it will compile just fine by using a default copy constructor.
dborba
Also, as I mentioned, it is common practice to have a copy constructor call an assignment operator, at which point compiler optimizations do come into play.
dborba
+1  A: 

double b1 = 0.5; is implicit call of constructor.

double b2(0.5); is explicit call.

Look at the following code to see the difference:

#include <iostream>
class sss { 
public: 
  explicit sss( int ) 
  { 
    std::cout << "int" << std::endl;
  };
  sss( double ) 
  {
    std::cout << "double" << std::endl;
  };
};

int main() 
{ 
  sss ddd( 7 ); // calls int constructor 
  sss xxx = 7;  // calls double constructor 
  return 0;
}

If your class has no explicit constuctors than explicit and implicit calls are identical.

Kirill V. Lyadvinsky
+1. Nice answer. Good to also note the explicit version. By the way, it's important to note that you can't have *both* versions of a single constructor overload at the same time. So, it would just fail to compile in the explicit case. If they both compile, they have to behave similarly.
Mehrdad Afshari
+1  A: 

First grouping: it depends on what A_factory_func returns. The first line is an example of copy initialization, the second line is direct initialization. If A_factory_func returns an A object then they are equivalent, they both call the copy constructor for A, otherwise the first version creates an rvalue of type A from an available conversion operators for the return type of A_factory_func or appropriate A constructors, and then calls the copy constructor to construct a1 from this temporary. The second version attempts to find a suitable constructor that takes whatever A_factory_func returns, or that takes something that the return value can be implicitly converted to.

Second grouping: exactly the same logic holds, except that built in types don't have any exotic constructors so they are, in practice, identical.

Third grouping: c1 is default initialized, c2 is copy-initialized from a value initialized temporary. Any members of c1 that have pod-type (or members of members, etc., etc.) may not be initialized if the user supplied default constructors (if any) do not explicitly initialize them. For c2, it depends on whether there is a user supplied copy constructor and whether that appropriately initializes those members, but the members of the temporary will all be initialized (zero-initialized if not otherwise explicitly initialized). As litb spotted, c3 is a trap. It's actually a function declaration.

Charles Bailey
+12  A: 
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.5/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.5/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.5/9). The second copy initializes: Value-initializes a temporary and then copies that value into c2 (Read 5.2.3/2 and 8.5/14). This of course will require a non-explicit copy constructor (Read 8.5/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 reurning 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 remarkable different in certain cases. The two forms of initialization, direct and copy inialization.

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.

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!

Johannes Schaub - litb
Good spot on the c3 trap!
Charles Bailey
Wow. I didn't even realize about the function declaration. I pretty much have to accept your answer just for being the only one to know about that.Is there a reason that function declarations work that way? It would be better if c3 were treated differently inside a function.
rlbond
POD == Plain Old Data?
quant_dev
@quant_dev, yes that's it
Johannes Schaub - litb
Bah, sorry folks, but i had to remove my comment and post it again, because of the new formatting engine: It's because in function parameters, `R() == R(*)()` and `T[] == T*` . That is, function types are function pointer types, and array types are pointer-to-element types. This sucks. It can be worked-around by `A c3((A()));` (parens around the expression).
Johannes Schaub - litb