Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Value initialization: MSVC vs clang

#include<cstddef>

template<typename T, std::size_t N>
struct A {
    T m_a[N];
    A() : m_a{} {}
};

struct S {
    explicit S(int i=4) {}
};

int main() {
    A<S, 3> an;
}

The above code compiles fine with MSVC (2017), but fails with clang 3.8.0 (Output of clang++ --version && clang++ -std=c++14 -Wall -pedantic main.cpp):

clang version 3.8.0 (tags/RELEASE_380/final 263969)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
main.cpp:6:15: error: chosen constructor is explicit in copy-initialization
    A() : m_a{} {}
              ^
main.cpp:14:13: note: in instantiation of member function 'A<S, 3>::A' requested here
    A<S, 3> an;
            ^
main.cpp:10:14: note: constructor declared here
    explicit S(int i=4) {}
             ^
main.cpp:6:15: note: in implicit initialization of array element 0 with omitted initializer
    A() : m_a{} {}
              ^
1 error generated.

clang 5.0 also refuses to compile this:

<source>:6:17: error: expected member name or ';' after declaration specifiers
    A() : m_a{} {}
                ^
<source>:6:14: error: expected '('
    A() : m_a{} {}
             ^
2 errors generated.

If I use simple parentheses in As constructor to (i.e. A() : m_a() {}), it compiles fine. From cppreference I would have suspected that both should result in the same (i.e. value initialization). Am I missing something or is this a bug in one of the compilers?

like image 251
phimuemue Avatar asked Feb 19 '18 16:02

phimuemue


3 Answers

Clang is correct.

Your confusion comes from:

From cppreference I would have suspected that both should result in the same (i.e. value initialization).

No they have different effects. Note the notes in that page:

In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.

That means when initialized with braced-init-list, for aggregate type, aggregate-initialization is preferred to be performed. With A() : m_a{} {}, and m_a is an array, which belongs to aggregate type, then aggregate initialization is performed instead:

(emphasis mine)

Each direct public base, (since C++17) array element, or non-static class member, in order of array subscript/appearance in the class definition, is copy-initialized from the corresponding clause of the initializer list.

and

If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default initializers, if provided in the class definition, and otherwise (since C++14) by empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates).

That means, the remaining elements, i.e. all the 3 elements of m_a will be copy-initialized from the empty list; for empty list the default constructor of S will be considered but it's declared as explicit; the copy-initialization won't invoke explicit constructors:

copy-list-initialization (both explicit and non-explicit constructors are considered, but only non-explicit constructors may be called)


On the other hand, A() : m_a() {} performs value initialization, then

3) if T is an array type, each element of the array is value-initialized;

then

1) if T is a class type with no default constructor or with a user-provided or deleted default constructor, the object is default-initialized;

then the default constructor of S is invoked to initialize the elements of m_a. Whether it's explicit or not doesn't matter for default initialization.

like image 117
songyuanyao Avatar answered Nov 20 '22 15:11

songyuanyao


For m_a{}:

  • [dcl.init]/17.1 sends us to [dcl.init.list], and [dcl.init.list]/3.4 says that we perform aggregate initialization on m_a per [dcl.init.aggr].

    The semantics of initializers are as follows. [...]

    • If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized.
    • [...]

    List-initialization of an object or reference of type T is defined as follows:

    • [...]
    • Otherwise, if T is an aggregate, aggregate initialization is performed.
    • [...]
  • [dcl.init.aggr]/5.2 says that we copy-initialize each element of m_a from an empty initializer list, i.e., {}.

    For a non-union aggregate, each element that is not an explicitly initialized element is initialized as follows:

    • [...]
    • Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).
    • [...]
  • This sends us back to [dcl.init]/17.1 for the initialization of each element, which again sends us to [dcl.init.list].
  • This time we hit [dcl.init.list]/3.5, which says that the element is value-initialized.

    List-initialization of an object or reference of type T is defined as follows:

    • [...]
    • Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.
    • [...]
  • Which brings us to [dcl.init]/8.1, which says that the element is default-initialized.

    To value-initialize an object of type T means:

    • if T is a (possibly cv-qualified) class type with either no default constructor ([class.ctor]) or a default constructor that is user-provided or deleted, then the object is default-initialized;
    • [...]
  • Which hits [dcl.init]/7.1, which says we enumerate constructors per [over.match.ctor] and perform overload resolution on the initializer ();

    To default-initialize an object of type T means:

    • If T is a (possibly cv-qualified) class type, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one for the initializer () is chosen through overload resolution. The constructor thus selected is called, with an empty argument list, to initialize the object.
    • [...]
  • and [over.match.ctor] says:

    For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization, the candidate functions are all the converting constructors of that class.

  • This default-initialization is in the context of copy-initialization, so the candidate functions are "all the converting constructors of that class".

  • The explicit default constructor is not a converting constructor. As a result, there is no viable constructor. Hence overload resolution fails, and the program is ill-formed.

For m_a():

  • We hit [dcl.init]/17.4, which says that the array is value-initialized.

    The semantics of initializers are as follows. [...]

    • [...]
    • If the initializer is (), the object is value-initialized.
    • [...]
  • Which brings us to [dcl.init]/8.3, which says that each element is value-initialized.

    To value-initialize an object of type T means:

    • [...]
    • if T is an array type, then each element is value-initialized;
    • [...]
  • Which again brings us to [dcl.init]/8.1, and then to [dcl.init]/7.1, and so we again enumerate constructors per [over.match.ctor] and perform overload resolution on the initializer ();

  • This time, the default-initialization is not in the context of copy-initialization, so the candidate functions are "all the constructors of the class of the object being initialized".
  • This time, the explicit default constructor is a candidate and selected by overload resolution. So the program is well-formed.
like image 3
T.C. Avatar answered Nov 20 '22 16:11

T.C.


This is explicitly ill-formed by the Standard (the question is, though, why?):

m_a{} list-initializes the S::m_a:

[dcl.init.list]/1

List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list, and the comma-separated initializer-clauses of the initializer-list or designated-initializer-clauses of the designated-initializer-list are called the elements of the initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization.

As an array, A<S, 3>::m_a is an aggregate type ([dcl.init.aggr]/1).

[dcl.init.aggr]/3.3

  1. When an aggregate is initialized by an initializer list as specified in [dcl.init.list], [...]
    3.3 the initializer list must be {}, and there are no explicitly initialized elements.

following, since there are no explicitly initialized elements:

[dcl.init.aggr]/5.2

  1. For a non-union aggregate, each element that is not an explicitly initialized element is initialized as follows: [...]
    5.2 if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).

Each S of A<S, 3>::m_a is, then, copy-initialized:

[dcl.init]/17.6.3

  1. The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined. [...]
    17.6 If the destination type is a (possibly cv-qualified) class type: [...]
    17.6.3 Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution. If the conversion cannot be done or is ambiguous, the initialization is ill-formed.

Since the default constructor of S is explicit, it cannot convert from the source type to the destination type (S).

The syntax using m_a() is, on the other hand, not aggregate member initialization and does not invoke copy-initialization.

like image 2
YSC Avatar answered Nov 20 '22 15:11

YSC