Do these two C++ initializer syntaxes ever differ in semantics?

438 views Asked by At

Assume that the following code is legal code that compiles properly, that T is a type name, and that x is the name of a variable.

Syntax one:

T a(x);

Syntax two:

T a = x;

Do the exact semantics of these two expressions ever differ? If so, under what circumstances?

If these two expressions ever do have different semantics I'm also really curious about which part of the standard talks about this.

Also, if there is a special case when T is the name of a scalar type (aka, int, long, double, etc...), what are the differences when T is a scalar type vs. a non-scalar type?

5

There are 5 answers

9
Tony Delroy On

From 8.5.14 (emphasis mine):

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 class.temporary, class.copy.

So, whether they're equivalent is left to the implementation.

8.5.11 is also relevant, but only in confirming that there can be a difference:

-11- The form of initialization (using parentheses or =) is generally insignificant, but does matter when the entity being initialized has a class type; see below. A parenthesized initializer can be a list of expressions only when the entity being initialized has a class type.

2
MSalters On

Yes. If the type of x is not T, then the second example expands to T a = T(x). This requires that T(T const&) is public. The first example doesn't invoke the copy constructor.

After the accessibility has been checked, the copy can be eliminated (as Tony pointed out). However, it cannot be eliminated before checking accessibility.

5
Pianosaurus On

The difference here is between implicit and explicit construction, and there can be difference.

Imagine having a type Array with the constructor Array(size_t length), and that somewhere else, you have a function count_elements(const Array& array). The purpose of these are easily understandable, and the code seems readable enough, until you realise it will allow you to call count_elements(2000). This is not only ugly code, but will also allocate an array 2000 elements long in memory for no reason.

In addition, you may have other types that are implicitly castable to an integer, allowing you to run count_elements() on those too, giving you completely useless results at a high cost to efficiency.

What you want to do here, is declare the Array(size_t length) an explicit constructor. This will disable the implicit conversions, and Array a = 2000 will no longer be legal syntax.

This was only one example. Once you realise what the explicit keyword does, it is easy to dream up others.

0
Andreia Gaita On

In C++, when you write this:

class A {
  public:
  A() { ... }
};

The compiler actually generates this, depending on what your code uses:

class A {
  public:
  A() { ... }
  ~A() { ... }
  A(const A& other) {...}
  A& operator=(const A& other) { ... }
};

So now you can see the different semantics of the various constructors.

A a1; // default constructor
A a2(a1); // copy constructor
a2 = a1; // copy assignment operator

The copy constructors basically copy all the non-static data. They are only generated if the resulting code is legal and sane: if the compiler sees types inside the class that he doesn't know how to copy (per normal assignment rules), then the copy constructor won't get generated. This means that if the T type doesn't support constructors, or if one of the public fields of the class is const or a reference type, for instance, the generator won't create them - and the code won't build. Templates are expanded at build time, so if the resulting code isn't buildable, it'll fail. And sometimes it fails loudly and very cryptically.

If you define a constructor (or destructor) in a class, the generator won't generate a default one. This means you can override the default generated constructors. You can make them private (they're public by default), you can override them so they do nothing (useful for saving memory and avoiding side-effects), etc.

0
Maxim Egorushkin On

T a(x) is direct initialization and T a = x is copy initialization.

From the standard:

8.5.11 The form of initialization (using parentheses or =) is generally insignificant, but does matter when the entity being initialized has a class type; see below. A parenthesized initializer can be a list of expressions only when the entity being initialized has a class type.

8.5.12 The initialization that occurs in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and brace-enclosed initializer lists (8.5.1) is called copy-initialization and is equivalent to the form

   T x = a;

The initialization that occurs in new expressions (5.3.4), static_cast expressions (5.2.9), functional notation type conversions (5.2.3), and base and member initializers (12.6.2) is called direct-initialization and is equivalent to the form

    T x(a);

The difference is that copy initialization creates a temporary object which is then used to direct-initialize. The compiler is allowed to avoid creating the temporary object:

8.5.14 ... 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 requires a non-explicit constructor and a copy constructor to be available.