Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

c++11

For example, I cannot write this:

class A {     vector<int> v(12, 1); }; 

I can only write this:

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

What's the consideration for the differences in C++11 language design?

like image 375
delphifirst Avatar asked Jul 19 '14 03:07

delphifirst


2 Answers

The rationale behind this choice is explicitly mentioned in the related proposal for non static data member initializers :

An issue raised in Kona regarding scope of identifiers:

During discussion in the Core Working Group at the September ’07 meeting in Kona, a question arose about the scope of identifiers in the initializer. Do we want to allow class scope with the possibility of forward lookup; or do we want to require that the initializers be well-defined at the point that they’re parsed?

What’s desired:

The motivation for class-scope lookup is that we’d like to be able to put anything in a non-static data member’s initializer that we could put in a mem-initializer without significantly changing the semantics (modulo direct initialization vs. copy initialization):

int x();  struct S {     int i;     S() : i(x()) {} // currently well-formed, uses S::x()     // ...     static int x(); };  struct T {     int i = x(); // should use T::x(), ::x() would be a surprise     // ...     static int x(); }; 

Problem 1:

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, for example:

HashingFunction hash_algorithm{"MD5"}; 

Here, we could not use the = form because HasningFunction’s constructor is explicit. In especially tricky cases, a type might have to be mentioned twice. Consider:

   vector<int> x = 3; // error:  the constructor taking an int is explicit    vector<int> x(3);  // three elements default-initialized    vector<int> x{3};  // one element with the value 3 

In that case, we have to chose between the two alternatives by using the appropriate notation:

vector<int> x = vector<int>(3); // rather than vector<int> x(3); vector<int> x{3}; // one element with the value 3 

Problem 2:

Another issue is that, because we propose no change to the rules for initializing static data members, adding the static keyword could make a well-formed initializer ill-formed:

   struct S {                const int i = f(); // well-formed with forward lookup         static const int j = f(); // always ill-formed for statics         // ...         constexpr static int f() { return 0; }     }; 

Problem 3:

A third issue is that class-scope lookup could turn a compile-time error into a run-time error:

struct S {     int i = j; // ill-formed without forward lookup, undefined behavior with     int j = 3; }; 

(Unless caught by the compiler, i might be intialized with the undefined value of j.)

The proposal:

CWG had a 6-to-3 straw poll in Kona in favor of class-scope lookup; and that is what this paper proposes, with initializers for non-static data members limited to the “= initializer-clause” and “{ initializer-list }” forms.

We believe:

Problem 1: This problem does not occur as we don’t propose the () notation. The = and {} initializer notations do not suffer from this problem.

Problem 2: adding the static keyword makes a number of differences, this being the least of them.

Problem 3: this is not a new problem, but is the same order-of-initialization problem that already exists with constructor initializers.

like image 197
Nikos Athanasiou Avatar answered Oct 09 '22 11:10

Nikos Athanasiou


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.

like image 32
Praetorian Avatar answered Oct 09 '22 11:10

Praetorian