41

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);
};

Why is there a difference between these two declaration syntaxes?

cigien
  • 57,834
  • 11
  • 73
  • 112
delphifirst
  • 1,781
  • 1
  • 14
  • 23
  • The former calls a vector constructor whose inputs are 12 and 1. The latter calls a vector constructor whose input is an initializer list. They are fundamentally different. – druckermanly Jul 19 '14 at 03:50
  • 1
    The standard-quotation reason is because the grammar is *declarator brace-or-equal-initializer(opt)* – chris Jul 19 '14 at 03:51

2 Answers2

38

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.

Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
25

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.

Praetorian
  • 106,671
  • 19
  • 240
  • 328
  • 1
    When used in parameter declaration, `foo()` is a function pointer other than a function itself, just as a built-in array declaration does. – Lingxi Jul 19 '14 at 05:34
  • @Lingxi Isn't that what I've said too? – Praetorian Jul 19 '14 at 05:37
  • I think logic can not reliably guide concerning small details of C++. For example, logically, since the list initialization can be written `v1{{12, 1}}`, the meaning of `v1{12,1}` could be chosen to support ordinary constructor call. That would be my choice as designer starting from "clean slate" here. ;-) – Cheers and hth. - Alf Jul 19 '14 at 05:38
  • @Praetorian In your original statement, it sounds somewhat like a reference to function to me. Not much of a problem, really. – Lingxi Jul 19 '14 at 05:42
  • @Cheers Agreed, the decisions could've been made differently. But as it stands, you [should be careful](http://stackoverflow.com/a/24112395/241631) using nested braces where they're not required :) – Praetorian Jul 19 '14 at 05:43
  • 1
    How is this any worse than the most vexing parse appearing elsewhere? – Ben Voigt Jul 19 '14 at 05:43
  • @Ben It's not any worse, and you're still allowed to write as many vexing parses as your heart desires. But the meaning of the old syntax cannot be changed, so the only option was to come up with new syntax that has an unambiguous meaning. Of course, all of this is my personal take on this. Maybe if someone were to dig through the C++ proposals for this feature, they might find the rationale. – Praetorian Jul 19 '14 at 05:46
  • @Praetorian: thanks, although i was aware of that (i answered that particular question in [a comment](http://stackoverflow.com/questions/24112281/c11-initializer-list-fails-but-only-on-lists-of-length-2/24112395#comment37197715_24112281), a bit hastily but enough). – Cheers and hth. - Alf Jul 19 '14 at 06:39
  • @Praetorian Thank you very much. But I found another problem: vector v1 contains twelve "1"s, not two elements "12" and "1". The situation seems different when v1 is a function local variable and when v1 is a class member. I use VC++2013 by the way. – delphifirst Jul 19 '14 at 12:24
  • @Cheersandhth.-Alf I found that if v1 is a class member, C++ works just in the way you said. By the way I use VC++2013. – delphifirst Jul 19 '14 at 12:29
  • @delphifirst It's yet another bug involving `initializer_list`. Here's the [bug report](https://connect.microsoft.com/VisualStudio/feedback/details/807232/class-member-vector-initialization-list-unexpected-c-11). – Praetorian Jul 19 '14 at 16:49
  • @Praetorian Thank you. This is the first time I encounter a problem related to compiler :-D – delphifirst Jul 20 '14 at 02:35