8

I always thought that when I use initializer list C++ syntax like:

something({ ... });

it's always clear to the compiler that I want to call the overload taking an std::initializer_list, but it seems this is not so clear for MSVC 2015.

I tested this simple code:

#include <cstdio>
#include <initializer_list>

namespace testing {
  template<typename T>
  struct Test {
    Test() {
      printf("Test::Test()\n");
    }

    explicit Test(size_t count) {
      printf("Test::Test(int)\n");
    }

    Test(std::initializer_list<T> init) {
      printf("Test::Test(std::initializer_list)\n");
    }

    T* member;
  };

  struct IntSimilar {
    int val;

    IntSimilar() : val(0) {}
    IntSimilar(int v) : val(v) {}

    operator int() {
      return val;
    }
  };
}

int main() {
    testing::Test<testing::IntSimilar> obj({ 10 });
    return 0;
}

Run

and in GCC 6.3 it works as expected, calling Test::Test(std::initializer_list)

but in MSVC 2015 this code calls Test::Test(int).

It seems MSVC can somehow ignore the {} and choose an invalid/unexpected overload to call.

What does the Standard say about this situation? Which version is valid?

Can anybody test this and confirm whether or not this issue remains in MSVC 2017?

underscore_d
  • 6,309
  • 3
  • 38
  • 64
crayze
  • 386
  • 3
  • 12
  • Which version of the standard do you use? – Revolver_Ocelot Dec 02 '17 at 13:30
  • Community 2015 Version 14.0.25123.0 Update 2 – crayze Dec 02 '17 at 13:32
  • 1
    No, which version of C++ standard you are compiling for? Is it C++14? C++17? C++11? – Revolver_Ocelot Dec 02 '17 at 13:35
  • Under MSVC 2017 constructor Test::Test(int) is invoked too. You can check it using this page https://godbolt.org/. – rafix07 Dec 02 '17 at 13:44
  • I don't know any option to switch in Visual Studio to choose "standard version", it's not gcc, I'm linking with standard platform toolset v140. Options->C/C++->Language does not provide too many options and non of them is standard version. I far as I know MSVC does not provide such option... – crayze Dec 02 '17 at 13:46
  • https://msdn.microsoft.com/en-us/library/mt490614.aspx – p-a-o-l-o Dec 02 '17 at 14:07
  • @p-a-o-l-o The document is old. Watch latest version here: https://learn.microsoft.com/en-us/cpp/build/reference/std-specify-language-standard-version – yumetodo Dec 02 '17 at 14:14
  • @p-a-o-l-o good to know. Switching between std:c++14 and std:c++latest has no any affect, MSVC choose Test(size_t count) anyway. – crayze Dec 02 '17 at 14:15
  • @yumetodo the link I provided refers to the version used by the OP – p-a-o-l-o Dec 02 '17 at 14:17
  • The behaviour of list initialization from one element changed several times over the 2011 - 2017 period – M.M Dec 03 '17 at 20:43

2 Answers2

4

Which version is valid?

According to my understanding of the standard, the GCC is right.

What does standard says about this situation?

What you do when you are writing Test obj1({10}); is direct-initializing an object of type Test with the expression { 10 }. During overload resolution, the compiler has to decide which constructor to call. According to 16.3.3.2 § 3 (3.1.1) [over.ics.rank]:

list-initialization sequence L1 is a better conversion sequence than list-initialization sequence L2 if L1 converts to std::initializer_list<X> for some X and L2 does not [...]

The standard also provides the example

void f1(int);                                 // #1
void f1(std::initializer_list<long>);         // #2
void g1() { f1({42}); }                       // chooses #2

This is the point where VS & clang differ from GCC: while all three will yield the same result in this particular example, changing it to

#include <iostream>

struct A { A(int) { } };
void f1(int) { std::cout << "int\n"; }                                // #1
void f1(std::initializer_list<A>) { std::cout << "list\n"; }          // #2

int main() {
    f1({42});
}

will let clang chose the int-constructor, moaning about the unnecessary braces around the literal 42 (which seems to be just in the standard for legacy reasons, see here) rather than checking if the { 42 } list sequence really cannot be converted to std::initializer_list<A>.

Note however that writing Test obj1{ 10 }; will lead to a different evaluation: According to the rules of list-initialization:

  • Otherwise, the constructors of T are considered, in two phases:
    • All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have default values, are examined, and matched by overload resolution against a single argument of type std::initializer_list

So the initializer_list constructor is taken for a special overload resolution stage considering only initializer_list constructors before the normal overload resolution is applied, as demonstrated in the famous std::vector-gotcha:

// will be a vector with elements 2, 0 rather than a vector of size 2 with values 0, 0
std::vector<int> v{ 2, 0 };

The fact that in both cases the standard decides to use the initializer_list constructor is a consistent choice, but technically, the reason for chosing it is quite different under the hood.

Jodocus
  • 7,493
  • 1
  • 29
  • 45
  • Good answer. The `f1({42});` example confirms what should happen. – wally Dec 02 '17 at 19:20
  • Sounds correct. Hopefully some people who are registered on the MSVC and Clang bugtrackers will see this! – underscore_d Dec 03 '17 at 12:35
  • Pedantic point: `{10}` is not an expression, however it is an *expression-list* grammatically (which is either a list of expressions, or a *braced-init-list*) – M.M Dec 03 '17 at 21:01
2

GCC is wrong here.

Indeed due to the parentheses it's direct-initialization so "normal" overloading rules apply, however, [over.ics.rank]/3.1 talk about this situation:

void f1(int);                                 // #1
void f1(std::initializer_list<long>);         // #2
void g1() { f1({42}); }                       // chooses #2

Whereas in our situation we have this:

struct IntSimilar { IntSimilar(int); };

void f1(size_t);                              // #1
void f1(std::initializer_list<IntSimilar>);   // #2
void g1() { f1({10}); }                       // chooses ?

And there is another rule, [over.ics.rank]/2 just before [over.ics.rank]/3:

— a standard conversion sequence is a better conversion sequence than a user-defined conversion

In order to invoke Test(initializer_list<IntSimilar>) a user-defined conversion is required (int to IntSimilar). But there is a better viable alternative, specifically just an integer conversion from int to size_t. That is possible because a scalar, such as an int, can be list-initialized from a braced-init-list with a single int element. See [dcl.init.list]/3.9:

— Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element ...

clang will in fact tell you exactly that (while selecting the int overload):

    warning: braces around scalar initializer [-Wbraced-scalar-init]

If you want to suppress automatic unwrapping of single-value braced-init-lists, either use list-initialization or wrap it into another braced-init-list:

    testing::Test<testing::IntSimilar> obj { 10 };
    testing::Test<testing::IntSimilar> obj({{10}});

- will select the initializer_list<T> overload everywhere.

rustyx
  • 80,671
  • 25
  • 200
  • 267
  • Section 3.9 is never entered for this case. `T obj({ 10 })` is not "list initialization of an object or reference of type T". (`T obj{10}` or `T obj = {10}` would be). Instead, start at the top of [dcl.init]. Point 17 (in the standard version you linked) , we go down to 17.6.2 which specifies that applicable constructors are enumerated and the best one is chosen by overload resolution. Point 17.1 does not apply because the initializer `({10})` is not a non-parenthesized braced-init-list. – M.M Dec 03 '17 at 20:58
  • I never said that `T` was initialized from `{ 10 }`. What I meant was that a temporary `int` was initialized from `{ 10 }`. – rustyx Dec 03 '17 at 21:38
  • Corresponding GCC bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99273 – Fedor Feb 04 '22 at 14:19