Home > Enterprise >  Behaviour of friend function template returning deduced dependent type in class template
Behaviour of friend function template returning deduced dependent type in class template

Time:06-17

I've happened across the following code, the behaviour of which is diagreed upon by all of GCC, Clang, and MSVC:

#include <concepts>

template<typename T>
auto foo(T);

template<typename U>
struct S {
    template<typename T>
    friend auto foo(T) {
        return U{};
    }
};

S<double> s;
static_assert(std::same_as<decltype(foo(42)), double>);

Live demo: https://godbolt.org/z/hK6xhesKM

foo() is declared at global namespace with deduced return type. S<U> provides a definition for foo() via a friend function, where it returns a value of type U.

What I expected is that when S is instantiated with U=double, its definition of foo() gets put in the global namespace and U is substituted in, due to how friend functions work, effectively like this:

template<typename T>
auto foo(T);

S<double> s;

template<typename T>
auto foo(T) {
    return double{};
}

Thus I expect foo()'s return type is double, and the following static assertion should pass:

static_assert(std::same_as<decltype(foo(42)), double>);

However, what actually happens is that all three compilers disagree on the behaviour.

GCC passes the static assertion, like I expect.

Clang fails the static assertion, as it seems to believe that foo() returns int:

'std::is_same_v<int, double>' evaluated to false

And MSVC produces a different error entirely:

'U': undeclared identifier

I find it bizarre that all the compilers have different behaviour here for a seemingly simple code example.
The issue does not occur if foo() isn't templated (see demo here), or if it doesn't have deduced return type (see demo here).

Which compiler has the correct behaviour? Or, is the code ill-formed NDR or undefined behaviour? (And, why?)

CodePudding user response:

Which compiler has the correct behaviour? Or, is the code ill-formed NDR or undefined behaviour? (And, why?)

As @Sedenion points out in a comment, whilst touching upon the domain of CWG 2118 (we'll return to this further down) this program is by the current standard well-formed and GCC is correct to accept it, whereas Clang and MSVC are incorrect to reject it, as governed by [dcl.spec.auto.general]/12 and /13 [emphasis mine]:

/12 Return type deduction for a templated entity that is a function or function template with a placeholder in its declared type occurs when the definition is instantiated even if the function body contains a return statement with a non-type-dependent operand.

/13 Redeclarations or specializations of a function or function template with a declared return type that uses a placeholder type shall also use that placeholder, not a deduced type. Similarly, redeclarations or specializations of a function or function template with a declared return type that does not use a placeholder type shall not use a placeholder.

[Example 6:

auto f();
auto f() { return 42; }            // return type is int
auto f();                          // OK
// ...
template <typename T> struct A {
  friend T frf(T);
};
auto frf(int i) { return i; }      // not a friend of A<int>

Particularly the "not a friend of A<int>" example of /13 highlights that the redeclaration shall use a placeholder type (frf(T), and not a deduced type (frf(int)), whereas otherwise that particular example would be valid.

/12, along with [temp.inst]/3 (below) covers the that return type deduction for the friend occurs only after the friend's primary template definition has been made available (formally: it's declaration but not definition has been instantiated) from the instantiation of the S<double> enclosing class template specialization.

The implicit instantiation of a class template specialization causes

  • (3.1) the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member classes, scoped member enumerations, static data members, member templates, and friends; and
  • [...]

However, for the purpose of determining whether an instantiated redeclaration is valid according to [basic.def.odr] and [class.mem], a declaration that corresponds to a definition in the template is considered to be a definition.

[Example 4:

// ...

template<typename T> struct Friendly {
  template<typename U> friend int f(U) { return sizeof(T); }
};
Friendly<char> fc;
Friendly<float> ff;  // error: produces second definition of f(U)

— end example]


CWG 2118: Stateful metaprogramming via friend injection

As covered in detail e.g. in A foliage of folly, one can rely on the single first instantiation of a class template to control how the definition of a friend looks like. First here means essentially relying on meta-programming state to specify this definition.

In OP's example the first instantiation, S<double>, is used to set the definition of the primary template of the friend foo such that its deduced type will always deduce to double, for all specializations of the friend. If we ever (in the whole program) instantiate, implicitly or explicitly, a second instantiation of S (ignoring S being specialized to remove the friend), we run into ODR-violations and undefined behavior. This means that this kind of program is, in practice, essentially useless, as it would serve clients undefined behavior on a platter, but as covered in the article linked to above it can be used for utility classes to circumvent private access rules (whilst still being entirely well-formed) or other hacky mechanisms such as stateful metaprogramming (which typically runs into or beyond the grey area of well-formed).

  • Related