C++ initializer list overload disambiguation
Asked Answered
G

1

10

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)
Gonium answered 25/2, 2021 at 16:24 Comment(0)
I
6

{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.

Instigation answered 25/2, 2021 at 21:15 Comment(1)
Thanks, this is clear. It is a pity that braces around a literal are accepted as the literal itself. It makes harder to disambiguate between aggregates and elementary types. Intuitively, {4} is perceived as a structured type, an aggregate, not an elementary type. Anyway, that is the rule...Gonium

© 2022 - 2024 — McMap. All rights reserved.