Search code examples
c++gccclanglanguage-lawyeroverload-resolution

Compiler variance for overloading over array reference parameters


The following program is, as expected, accepted by both GCC, Clang and MSVC (for various compiler and language versions):

// #g.1: rvalue reference function parameter
constexpr bool g(int&&)       { return true; }

// #g.2: const lvalue reference function parameter
constexpr bool g(int const&)  { return false; }

static_assert(g(0), ""); // OK: picks #g.1

The similar case but for overloads over an array {rvalue, const lvalue} reference parameter is, however, rejected by GCC, whilst it is accepted by Clang and MSVC:

#include <cstddef>

// #f.1: rvalue ref overload
template<std::size_t size>
constexpr bool f(int (&&)[size])      { return true; }

// #f.2: lvalue ref overload
template<std::size_t size>
constexpr bool f(int const (&)[size]) { return false; }

static_assert(f({1, 2, 3}), ""); // Clang: OK    (picks #f.1)
                                 // MSVC:  OK    (picks #f.1)
                                 // GCC:   Error (picks #f.2)

DEMO.

At first glance this looks like a GCC bug, but I'd like to be sure.

Question

  • Which compiler(s) is(/are) right here?

Solution

  • GCC is wrong

    Since the argument is an initializer list, special rules apply, as per [over.ics.list]/1:

    When an argument is an initializer list ([dcl.init.list]), it is not an expression and special rules apply for converting it to a parameter type.

    As the parameter type for both overloads are references types, [over.ics.list]/9 applies [emphasis mine]:

    Otherwise, if the parameter is a reference, see [over.ics.ref].

    [Note 2: The rules in this subclause will apply for initializing the underlying temporary for the reference. — end note]

    Albeit non-normative, this means that the call

    f({1, 2, 3})
    

    for both of the candidate functions is (as per [over.ics.list]/6) as if

    template<typename T>
    using type = T;
    
    f(type<int[3]>{1, 2, 3});  // prvalue array
    

    [over.ics.ref], particularly [over.ics.ref]/3, simply covers that this kind of prvalue argument can bind directly both to an rvalue reference or to a const lvalue reference, meaning both candidates are viable candidates.

    We turn to [over.ics.ran]/3.2.3:

    /3.2 Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if

    • [...]
    • /3.2.3 S1 and S2 include reference bindings ([dcl.init.ref]) and neither refers to an implicit object parameter of a non-static member function declared without a ref-qualifier, and S1 binds an rvalue reference to an rvalue and S2 binds an lvalue reference

    Thus, GCC is wrong to pick the const lvalue reference overload #f.2, as #f.1 is the best viable function.


    We may note that all compilers agree on the #f.1 overload if we explicitly pass an rvalue array instead of using an initializer list (DEMO):

    template<typename T>
    using type = T;
    
    // All compilers agree: OK
    static_assert(f(type<int[3]>{1, 2, 3}));
    

    GCC's invalid behavior in the original example is thus arguably specifically due to how GCC treats initializer list arguments for candidate functions whose corresponding function parameter type is of array reference type.


    Bug report

    • Bug 104996 - Overload resolution over rvalue/const lvalue array reference parameters for an init. list argument incorrectly picks the const lvalue ref. overload