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?
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 sequenceL2
ifL1
converts tostd::initializer_list<X>
for someX
andL2
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With