Why Class Data Members Can't Be Initialized by Direct Initialization Syntax

Why class data members can't be initialized by direct initialization syntax?

Early proposals leading to the feature's introduction explain that this is to avoid parsing problems.

Here's just one of the examples presented therein:

Unfortunately, this makes initializers of the “( expression-list )” form ambiguous at the time that the declaration is being parsed:

struct S {
int i(x); // data member with initializer
// ...
static int x;
};

struct T {
int i(x); // member function declaration
// ...
typedef int x;
};

One possible solution is to rely on the existing rule that, if a declaration could be an object or a function, then it’s a function:

struct S {
int i(j); // ill-formed...parsed as a member function,
// type j looked up but not found
// ...
static int j;
};

A similar solution would be to apply another existing rule, currently used only in templates, that if T could be a type or something else, then it’s something else; and we can use “typename” if we really mean a type:

struct S {
int i(x); // unabmiguously a data member
int j(typename y); // unabmiguously a member function
};

Both of those solutions introduce subtleties that are likely to be misunderstood by many users (as evidenced by the many questions on comp.lang.c++ about why “int i();” at block scope doesn’t declare a default-initialized int).

The solution proposed in this paper is to allow only initializers of the “= initializer-clause” and “{ initializer-list }” forms. That solves the ambiguity problem in most cases. [..]

Why I can't use constructor initializer list to initialize a in-class struct?

In your 2-param constructor, you can use aggregate initialization of the inner struct, eg:

test(int x, int y): _val(x), inclassobj{y}{}

Online Demo

In your second example, adding a constructor to the inner struct is fine, you just need to call it in the outer class's constructor member initialization list, eg:

test(int x, int y): _val(x), inclassobj(y){}

Online Demo

Initialization of member variable via parentheses doesn't work

There isnt actually much to explain, its just not valid syntax. Default member initializers are

class X
{
private:
int data{1}; // ok
int data2 = 42; // also ok

public:
void print()
{
cout << data << endl;
}
};

While int data(1); is not valid syntax for a default member initializer. See here for details: https://en.cppreference.com/w/cpp/language/data_members

How do C++ class members get initialized if I don't do it explicitly?

In lieu of explicit initialization, initialization of members in classes works identically to initialization of local variables in functions.

For objects, their default constructor is called. For example, for std::string, the default constructor sets it to an empty string. If the object's class does not have a default constructor, it will be a compile error if you do not explicitly initialize it.

For primitive types (pointers, ints, etc), they are not initialized -- they contain whatever arbitrary junk happened to be at that memory location previously.

For references (e.g. std::string&), it is illegal not to initialize them, and your compiler will complain and refuse to compile such code. References must always be initialized.

So, in your specific case, if they are not explicitly initialized:

    int *ptr;  // Contains junk
string name; // Empty string
string *pname; // Contains junk
string &rname; // Compile error
const string &crname; // Compile error
int age; // Contains junk

Why can't we initialize class members at their declaration?

The initialization of non-static members could not be done like this prior to C++11. If you compile with a C++11 compiler, it should happily accept the code you have given.

I imagine that the reason for not allowing it in the first place is because a data member declaration is not a definition. There is no object being introduced. If you have a data member such as int x;, no int object is created until you actually create an object of the type of the class. Therefore, an initializer on this member would be misleading. It is only during construction that a value can be assigned to the member, which is precisely what member initialization lists are for.

There were also some technical issues to iron out before non-static member initialization could be added. Consider the following examples:

struct S {
int i(x);
// ...
static int x;
};

struct T {
int i(x);
// ...
typedef int x;
};

When these structs are being parsed, at the time of parsing the member i, it is ambiguous whether it is a data member declaration (as in S) or a member function declaration (as in T).

With the added functionality, this is not a problem because you cannot initialize a member with this parantheses syntax. You must use a brace-or-equal-initializer such as:

int i = x;
int i{x};

These can only be data members and so we have no problem any more.

See the proposal N2628 for a more thorough look at the issues that had to be considered when proposing non-static member initializers.

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.



Related Topics



Leave a reply



Submit