Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 478
dfrib Avatar asked Nov 09 '20 20:11

dfrib


2 Answers

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.

like image 147
Jarod42 Avatar answered Nov 02 '22 01:11

Jarod42


Different trailing requires-clauses distinguish declarations of hidden friends for different specializations from eachother

Clang and GCC are both wrong to reject the program. As pointed out in @Jared42:s answer, [over.dcl]/1 should likewise apply for hidden friend declarations, such that the declarations in the OP:s example declare different friend functions.

Associated Clang bug report:

  • Bug 48872 - Rejects-valid for overloaded hidden non-template friend functions with mutually exclusive requires-clauses

Associated GCC bug report:

  • Bug 98822 - Rejects-valid: instantiation of class template instantiates (all) constrained non-template friend definitions (, even those) with unsatisfied constraints

Details

GCC is wrong by violation of [temp.friend]/9:

A non-template friend declaration with a requires-clause shall be a definition. A friend function template with a constraint that depends on a template parameter from an enclosing template shall be a definition. Such a constrained friend function or function template declaration does not declare the same function or function template as a declaration in any other scope.

It was not clear to me at first that this separates friend declarations with specialization-exclusive requires-clauses, but as commented by the author of the paragraph (Hubert Tong; see details below), this is the intent of the paragraph:

[...] the "does not declare" wording is meant to say that the friends declared by each specialization is unique.

Clang is wrong by violation of [defns.signature.friend], which includes a trailing requires-clause (if any) in the signature of a non-template friend function [emphasis mine]:

⟨non-template friend function with trailing requires-clause⟩ name, parameter-type-list, enclosing class, and trailing requires-clause

meaning Clang should not generate the same mangled name for the two, separate (by separate requires-clauses) friend declarations.

MSVC is likely also wrong to the fail program (E) in the stage of overload resolution (with an ambiguity error), as e.g. foo(s1) should arguably only add candidates from S<1> to its candidate functions. Whether constraints checking can actually be applied for an argument that is Base& and not a particular specialization of S is another question, but the possible error should not be one of ambiguity but rather inability to fulfill the constraints of a candidate function.

US115: Hidden non-template friends need a requires-clause

US115 of P2103R0 (Core Language Changes for NB Comments at the February, 2020 (Prague) meeting) proposed updating the standard rules for non-template hidden friends such that (also) non-template friend (functions) should be allowed to use trailing requires-clauses:

US115. Hidden non-template friends need a requires-clause

  1. Add the following after 3.20 [defns.signature]:

3.21 [defns.signature.friend]

signature

‹non-template friend function with trailing requires-clause› name, parameter-type-list (9.3.3.5 [dcl.fct]), enclosing class, and trailing requires-clause (9.3 [dcl.decl])

  1. Add the following after 3.21 [defns.signature.templ]:

3.23 [defns.signature.templ.friend]

signature

‹friend function template with constraint involving enclosing template parameters› name, parameter-type-list (9.3.3.5 [dcl.fct]), return type, enclosing class, template-head, and trailing requires-clause

  1. Change 13.7.4 [temp.friend] paragraph 9 as follows:

A non-template friend declaration shall not have with a requires-clause shall be a definition. A friend function template with a constraint that depends on a template parameter from an enclosing template shall be a definition. Such a constrained friend function or function template declaration does not declare the same function or function template as a declaration in any other scope.

the majority of the change affection an expansion of [temp.friend]/9.

US115 was recorded as issue 114 in cplusplus/nbballot:

US115 13.6.4 [temp.friend] Hidden non-template friends need a requires-clause

Hidden friends that are non-templates currently cannot have a requires-clause, but this functionality is important and used throughout Ranges.

Proposed change:

Change [temp.friend]/9 to refer only to those friend declarations that are not any kind of templated entity.

and was implemented in pull request #3782 to the standard draft, particularly as per the following commit:

NB US 115 (C++20 CD): Hidden non-template friends need a requires-clause

Added obviously-missing (if any) to the mention of a trailing requires-clause in the definition of signature for a friend function template.

I asked for clarification (given the different implementations of the GCC, Clang, MSVC) regarding the expanded rule of [temp.friend]/9 w.r.t. overloading hidden non-template friends solely based differences in trailing requires-clauses, with the answer that this should (likely) be legal, and that both GCC and Clang are wrong to reject example (E) in their respective manners (it should be an overload resolution ambiguity error:

Hubert Tong (hubert-reinterpretcast)

I think MSVC is correct here. With respect to the Clang behaviour, the description of the signature indicates that the mangling should be unique. With respect to the GCC behaviour, the "does not declare" wording is meant to say that the friends declared by each specialization is unique.

like image 25
dfrib Avatar answered Nov 02 '22 02:11

dfrib