Search code examples
c++templateslanguage-lawyerc++20friend-function

Overloading of hidden friends by differences only in (mutually exclusive) requires-clauses: legal or an ODR-violation?


Consider the following class template, which contains two (hidden) friend declarations of the same friend (same function type; see below), which also defines the friend (and the friend is thus inline), but with the definition conditional on (mutually exclusive) requires-clauses:

#include <iostream>

struct Base {};

template<int N>
struct S : public Base {
    friend int foo(Base&) requires (N == 1) { return 1; }
    friend int foo(Base&) requires (N == 2) { return 3; }
};

[dcl.fct]/8 states that trailing requires-clauses are not part of a function's type [emphasis mine]:

The return type, the parameter-type-list, the ref-qualifier, the cv-qualifier-seq, and the exception specification, but not the default arguments ([dcl.fct.default]) or the trailing requires-clause ([dcl.decl]), are part of the function type.

which means that the two definitions above is an ODR-violation for a case where both definitions are instantiated; if we only focus on a single translation unit, [basic.def.odr]/1 would be violated:

No translation unit shall contain more than one definition of any variable, function, class type, enumeration type, template, default argument for a parameter (for a function in a given scope), or default template argument.

and in a single TU this violation should arguably be diagnosable (no "need" for "ill-formed, NDR"). I'm trying understand the rules for when the definitions above will be instantiated; or if this is entirely implementation-defined (or even ill-formed before reaching instantiation phase).

Both Clang and GCC(1) accepts the following program

// ... as above

// (A)
int main() {
    S<1> s1{};
    std::cout << foo(s1);  // Clang & GCC: 1
}

For programs (B) through (D) below, however, Clang accepts them all whereas GCC rejects them all with a re-definition error:

// (B)
int main() {
    S<1> s1{};
    S<2> s2{};  // GCC: re-definition error of 'foo'
}

// (C)
int main() {
    S<1> s1{};
    S<2> s2{};  // GCC: re-definition error of 'foo'
    std::cout << foo(s1);  // Clang: 1
}

// (D)
template struct S<1>;
template struct S<2>;  // GCC: re-definition error of 'foo'

int main() {}

It's only when actually trying to invoke the friend function via ADL on both specializations that Clang actually emits an error

// (E)
int main() {
    S<1> s1{};
    S<2> s2{};  // GCC: re-definition error of 'foo'
    std::cout << foo(s1); // MSVC: ambiguous call
    std::cout << foo(s2);  
    // Clang error: definition with same mangled name
    //              '_Z3fooR4Base' as another definition
} 

and we may note that only MSVC actually reaches a state of seemingly accepting both definitions after which it fails as expect ("ambiguous call").

DEMO.

Question

  • Can hidden non-template friend functions of class templates (friend declarations where the friends are also defined in the class) be overloaded only by a difference in (mutually exclusive) requires-clauses?

And, thus, which compiler is right here?

  1. All (ill-formed NDR and/or implementation defined w.r.t. point of instantiation rules),
  2. GCC
  3. Clang
  4. MSVC
  5. None (example (E) is well-formed)

I have not been able to understand what rules that governs when a friend function declaration (of a class template) that is also a definition, is instantiated, particularly when requires-clauses are involved; possibly this is irrelevant, though, if the behaviour of GCC and Clang above are both incorrect.


(1) GCC HEAD 11.0.0, Clang HEAD 12.0.0.


Solution

  • From over#dcl-1,

    Two function declarations of the same name refer to the same function if they are in the same scope and have equivalent parameter declarations ([over.load]) and equivalent ([temp.over.link]) trailing requires-clauses, if any ([dcl.decl]).

    [Note 1: Since a constraint-expression is an unevaluated operand, equivalence compares the expressions without evaluating them.
    [Example 1:
    template<int I> concept C = true;
    template<typename T> struct A {
    void f() requires C<42>; // #1
    void f() requires true; // OK, different functions
    };
    — end example]
    — end note]

    I understand there is 2 different foo (so no ODR violations) because of the differing requires clauses.

    I think there is issue with all mentioned compilers to not cover this corner case.