Why Can In-Class Initializers Only Use = or {}

Why can in-class initializers only use = or {}? [duplicate]

I am not 100% positive about this, but this might be to prevent a syntax ambiguity. For example, consider the following class:

class BadTimes {
struct Overloaded;
int Overloaded; // Legal, but a very strange idea.

int confusing(Overloaded); // <-- This line
};

What does the indicated line mean? As written, this is a declaration of a member function named confusing that accepts as a parameter an object of type Overloaded (whose name isn't specified in the function declaration) and returns an int. If C++11 were to allow initializers to use parentheses, this would be ambiguous, because it could also be a definition of a member of type int named confusing that is initialized to the value of the data member Overloaded. (This is related to the current issue with the Most Vexing Parse.)

By requiring curly braces, this ambiguity is removed:

class BadTimes {
struct Overloaded;
int Overloaded; // Legal, but a very strange idea.

int confusing{Overloaded}; // <-- This line
};

Now, it's clear that confusing is actually an int initialized to the value of Overloaded, because there's no way to read it as a function declaration.

Hope this helps!

Why C++11 in-class initializer cannot use parentheses?

One possible reason is that allowing parentheses would lead us back to the most vexing parse in no time. Consider the two types below:

struct foo {};
struct bar
{
bar(foo const&) {}
};

Now, you have a data member of type bar that you want to initialize, so you define it as

struct A
{
bar B(foo());
};

But what you've done above is declare a function named B that returns a bar object by value, and takes a single argument that's a function having the signature foo() (returns a foo and doesn't take any arguments).

Judging by the number and frequency of questions asked on StackOverflow that deal with this issue, this is something most C++ programmers find surprising and unintuitive. Adding the new brace-or-equal-initializer syntax was a chance to avoid this ambiguity and start with a clean slate, which is likely the reason the C++ committee chose to do so.

bar B{foo{}};
bar B = foo();

Both lines above declare an object named B of type bar, as expected.


Aside from the guesswork above, I'd like to point out that you're doing two vastly different things in your example above.

vector<int> v1{ 12, 1 };
vector<int> v2 = vector<int>(12, 1);

The first line initializes v1 to a vector that contains two elements, 12 and 1. The second creates a vector v2 that contains 12 elements, each initialized to 1.

Be careful of this rule - if a type defines a constructor that takes an initializer_list<T>, then that constructor is always considered first when the initializer for the type is a braced-init-list. The other constructors will be considered only if the one taking the initializer_list is not viable.

C++11 member initializer list vs in-class initializer?

No, these are not the same.

The difference between them is the same that applies for direct-initialization vs. copy-initialization, which is subtle but often very confusing.

§12.6.2 [class.base.init]:


  1. The expression-list or braced-init-list in a mem-initializer is used to initialize the designated subobject (or, in the case of a delegating constructor, the complete class object) according to the initialization rules of 8.5 for direct-initialization. [...]

  2. In a non-delegating constructor, if a given non-static data member or base class is not designated by a mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no ctor-initializer) and the entity is not a virtual base class of an abstract class (10.4), then

    — if the entity is a non-static data member that has a brace-or-equal-initializer, the entity is initialized as specified in 8.5;

§8.5 [dcl.init]:


  1. The initialization that occurs in the form

    T x = a;


as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization.

Initializing a non-static data member on a member-initializer-list follows the rules of direct-initialization, which doesn't create intermediate temporaries that need to be moved/copied (if compiled without a copy-elision), neither the type of the data member must be copyable/movable (even if the copy is elided). In addition, a direct-initialization introduces an explicit context, while a copy-initialization is non-explicit (if a constructor selected for the initialization is explicit, the program won't compile).

In other words, the obj s = obj("value"); syntax won't compile if obj is declared as:

struct obj
{
obj(std::string) {}
obj(const obj&) = delete;
};

or:

struct obj
{
obj(std::string) {}
explicit obj(const obj&) {}
};

As a more tangible example, while the below won't compile:

struct any
{
std::atomic<int> a = std::atomic<int>(1); // ill-formed: non-copyable/non-movable
std::atomic<int> b = 2; // ill-formed: explicit constructor selected
};

this one will:

struct any
{
std::atomic<int> a;
std::atomic<int> b{ 2 };
any() : a(1) {}
};

Which way is better (performance) ?

With a copy-elision enabled both have identical performance. With copy-elision disabled, there is an additional copy/move constructor call upon every instantiation when the copy-initialization syntax is used (that obj s = obj("value"); is one of).


Is there another way ?

The brace-or-equal-initializer syntax allows one to perform a direct-list-initialization as well:

class any {
public:
obj s{ "value" };
any() {}
};

Are there any other differences?

Some other differences that are worth mentioning are:

  1. Brace-or-equal-initializer must reside in a header file along with a class declaration.
  2. If both are combined, member-initializer-list takes priority over brace-or-equal-initializer (that is, brace-or-equal-initializer is ignored).
  3. (C++11 only, until C++14) A class that uses brace-or-equal-initializer violates constraints for an aggregate type.
  4. With the brace-or-equal-initializer syntax it's not possible to perform a direct-initialization other than a direct-list-initialization.

What's the reason for initializing fields inside class?

From the authority (this reads pretty similar to the earlier standard-proposal N2756):

In-class member initializers


In C++98, only static const members of integral types can be initialized in-class, and the initializer has to be a constant expression. These restrictions ensure that we can do the initialization at compile-time. For example:

int var = 7;

class X {
static const int m1 = 7; // ok
const int m2 = 7; // error: not static
static int m3 = 7; // error: not const
static const int m4 = var; // error: initializer not constant expression
static const string m5 = "odd"; // error: not integral type
// ...
};

The basic idea for C++11 is to allow a non-static data member to be initialized where it is declared (in its class). A constructor can then use the initializer when run-time initialization is needed. Consider:

class A {
public:
int a = 7;
};

This is equivalent to:

class A {
public:
int a;
A() : a(7) {}
};

This saves a bit of typing, but the real benefits come in classes with multiple constructors. Often, all constructors use a common initializer for a member:

class A {
public:
A(): a(7), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(int a_val) : a(a_val), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(D d) : a(7), b(g(d)), hash_algorithm("MD5"), s("Constructor run") {}
int a, b;
private:
HashingFunction hash_algorithm; // Cryptographic hash to be applied to all A instances
std::string s; // String indicating state in object lifecycle
};

The fact that hash_algorithm and s each has a single default is lost in the mess of code and could easily become a problem during maintenance. Instead, we can factor out the initialization of the data members:

class A {
public:
A(): a(7), b(5) {}
A(int a_val) : a(a_val), b(5) {}
A(D d) : a(7), b(g(d)) {}
int a, b;
private:
HashingFunction hash_algorithm{"MD5"}; // Cryptographic hash to be applied to all A instances
std::string s{"Constructor run"}; // String indicating state in object lifecycle
};

If a member is initialized by both an in-class initializer and a constructor, only the constructor's initialization is done (it "overrides" the default). So we can simplify further:

class A {
public:
A() {}
A(int a_val) : a(a_val) {}
A(D d) : b(g(d)) {}
int a = 7;
int b = 5;
private:
HashingFunction hash_algorithm{"MD5"}; // Cryptographic hash to be applied to all A instances
std::string s{"Constructor run"}; // String indicating state in object lifecycle
};

Order of In Class Initialization versus Constructor Initialization List

This is guaranteed by the standard that non-static data members will be initialized in the order of their declarations in the class definition. How they're initialized (via default member initializer or member initializer list) and the order of these initializers don't matter.

[class.base.init]#13.3

(13.3) - Then, non-static data members are initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).

[ Note: The declaration order is mandated to ensure that base and member subobjects are destroyed in the reverse order of initialization. — end note ]

That means, the initialization order will always be ptr -> m1 -> m2.

Why in-class initializer can't use ( ) [duplicate]

It is disallowed by the language. The reason is that there would be cases where it couldn't be disambiguated from a function declaration:

struct foo
{
int bar();
};

So instead of replicating the whole most vexing parse fiasco by allowing () to work sometimes, it is outright disallowed.

What is the order of initialization in class members?

C++ initialization takes place in this order:

  1. Base classes, from left to right
  2. Member variables, in the order in which they are declared

The initialization of base classes from step 1 recursively takes the same steps. Therefore, all bases are fully constructed before any member variable initialization takes place, and before the body of the constructor begins execution.

Therefore, when the compiler encounters:

Two two;

First, Two::Two begins execution, starting with the initializer list. All bases are initialized via the initialization list, even if you haven't written one or left out the initialization of a base class. So, the code that actually runs looks more like this:

Two::Two
:
One(),
test()
{
test = "text";
}

The initializer list is executed before the body of the constructor. Therefore, One is completely constructed before the the body of Two::Two begins execution.

In turn, One looks like this:

One::One()
:
Base()
{
string test = "test";
}

And Base is empty:

Base::Base()
{
}

So what happens when Two two; is executed is:

  1. Base is constructed.
  2. One is constructed
  3. The automatic variable test is constructed, initialized, and destroyed in the context of One::One
  4. Two::test is default-initialized
  5. Two::test is assigned the value "text"

Note that some of this, especially steps 4 & 5 might get optimized by the compiler if it thinks it's safe to do so.



Related Topics



Leave a reply



Submit