I have a question about C++ initializer list disambiguation which exhibits different behaviors between gcc, clang and Visual Studio.
I wonder if this is "undefined behavior" (incorrect program) or if one of these compilers has a bug. Any idea?
Consider the following declarations:
class Arg
{
public:
Arg(int i);
};
class Object
{
public:
Object(const char* str, int i);
Object(const char* str, const std::initializer_list<Arg>& args);
};
And now this usage:
Object c("c", {4});
Which constructor should be used? The one with int
(assuming that the braces around the literal are superfluous) or the one with the initializer list (with implicit conversion from int
to Arg
).
GCC 10.2.0 chooses the constructor with the initializer list of Arg
.
Clang 11.2.2-2 chooses the constructor with int
and reports a warning about the braces:
initlist.cpp:46:19: warning: braces around scalar initializer [-Wbraced-scalar-init]
Object c("c", {4});
^~~
Visual Studio 2019 16.8.6 chooses the constructor with int
without warning (/W4
).
On a majority standpoint, the constructor with int
wins. On the other hand, if we directly use std::initializer_list<int>
instead of std::initializer_list<Arg>
(no implicit call to Arg
constructor), all three compilers choose the constructor with the initializer list.
Because of the ambiguity and the difference of behavior, this kind of code should be avoided anyway. But I am curious to understand who is wrong? The undefined application code or one of the compilers?
Full source code below, in case anyone wants to try:
#include <iostream>
class Arg
{
public:
int value;
Arg(int i);
};
class Object
{
public:
Object(const char* str, int i);
Object(const char* str, const std::initializer_list<Arg>& args);
};
Arg::Arg(int i) : value(i)
{
std::cout << "Arg(" << i << ")" << std::endl;
}
Object::Object(const char* str, int i)
{
std::cout << "Object(\"" << str << "\", " << i << ")" << std::endl;
}
Object::Object(const char* str, const std::initializer_list<Arg>& args)
{
std::cout << "Object(\"" << str << "\", {";
bool comma = false;
for (auto it = args.begin(); it != args.end(); ++it) {
if (comma) {
std::cout << ", ";
}
comma = true;
std::cout << it->value;
}
std::cout << "})" << std::endl;
}
int main(int argc, char* argv[])
{
Object a("a", 1);
Object b("b", {2, 3});
Object c("c", {4});
}
With GCC:
Object("a", 1)
Arg(2)
Arg(3)
Object("b", {2, 3})
Arg(4)
Object("c", {4})
With clang and VS:
Object("a", 1)
Arg(2)
Arg(3)
Object("b", {2, 3})
Object("c", 4)
(not to be confused with member initializer list ) An object of type std::initializer_list<T> is a lightweight proxy object that provides access to an array of objects of type const T.
Before the compound statement that forms the function body of the constructor begins executing, initialization of all direct bases, virtual bases, and non-static data members is finished. Member initializer list is the place where non-default initialization of these objects can be specified.
To solve this problem, C++ provides a method for initializing class member variables (rather than assigning values to them after they are created) via a member initializer list (often called a “member initialization list”). Do not confuse these with the similarly named initializer list that we can use to assign values to arrays.
Reference members must be initialized using Initializer List. In the following example, “t” is a reference member of Test class and is initialized using Initializer List. In the following example, an object “a” of class “A” is data member of class “B”, and “A” doesn’t have default constructor. Initializer List must be used to initialize “a”.
{4}
to const std::initializer_list<Arg>&
is a user-defined conversion.
{4}
to int
is a standard conversion.
The latter wins. This is a GCC bug.
List-initialization to an initializer_list
beats others when the conversion sequences are of the same form. They are not here.
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