Search code examples
c++c++11initializer-listoverload-resolution

Overload resolution with an empty brace initializer: pointer or reference?


I ran into a real-life WTF moment when I discovered that the code below outputs "pointer".

#include <iostream>
#include <utility>

template<typename T>
struct bla
{
    static void f(const T*) { std::cout << "pointer\n"; }
    static void f(const T&) { std::cout << "reference\n"; }
};

int main()
{
    bla<std::pair<int,int>>::f({});
}

Changing the std::pair<int,int> template argument to an int or any other primitive type, gives the (for me at least) expected "ambiguous overload" error. It seems that builtin types are special here, because any user-defined type (aggregate, non-trivial, with defaulted constructor, etc...) all lead to the pointer overload being called. I believe the template is not necessary to reproduce it, it just makes it simple to try out different types.

Personally, I don't think that is logical and I would expect the ambiguous overload error in all cases, regardless of the template argument. GCC and Clang (and I believe MSVC) all disagree with me, across C++11/14/1z. Note I am fully aware of the bad API these two overloads present, and I would never write something like this, I promise.

So the question becomes: what is going on?


Solution

  • Oh, this is nasty.

    Per [over.ics.list]p4 and p7:

    4 Otherwise, if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single best constructor of X to perform the initialization of an object of type X from the argument initializer list, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion. [...]

    [...]

    6 Otherwise, if the parameter is a reference, see 13.3.3.1.4. [Note: The rules in this section will apply for initializing the underlying temporary for the reference. -- end note] [...]

    [...]

    7 Otherwise, if the parameter type is not a class:

    [...]

    (7.2) -- if the initializer list has no elements, the implicit conversion sequence is the identity conversion. [...]

    The construction of a const std::pair<int,int> temporary from {} is considered a user-defined conversion. The construction of a const std::pair<int,int> * prvalue, or a const int * prvalue, or a const int temporary object are all considered standard conversions.

    Standard conversions are preferred over user-defined conversions.

    Your own find of CWG issue 1536 is relevant, but mostly for language lawyers. It's a gap in the wording, where the standard doesn't really say what happens for initialisation of a reference parameter from {}, since {} is not an expression. It's not what makes the one call ambiguous and the other not though, and implementations are managing to apply common sense here.