Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wrong overload called when constructing from initializer_list inside parentheses

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?

like image 875
crayze Avatar asked Dec 02 '17 13:12

crayze


2 Answers

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.

like image 195
Jodocus Avatar answered Nov 18 '22 21:11

Jodocus


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.

like image 1
rustyx Avatar answered Nov 18 '22 22:11

rustyx