c++c++20friend-functiontrailing-return-typerequires-clause

accessing member in trailing return type vs. in requires clause of a locally defined friend function


Example code as below or on godbolt. All friend functions compile with gcc and Visual Studio. clang fails when trying to access S<T>::foo() in trailing return type.

If clang is correct, why is member access into incomplete type not allowed in trailing return type but allowed in requires clause?

#include <concepts>

template<typename T>
struct S {
    T m_t;

    T const& foo() const& { return m_t; }

    friend auto bar_auto(S const& s) {
        return s.foo();
    }

    friend decltype(auto) bar_decltype_auto(S const& s) {
        return s.foo();
    }

    friend decltype(auto) bar_requires(S const& s) requires std::same_as<T const&, decltype(s.foo())> {
        return s.foo();
    }

    friend auto bar_trailing_return(S const& s) -> decltype(s.foo()) { // clang error: member access into incomplete type 'const S<int>'
        return s.foo();
    }
};

S<int> s{1};

This is a confirmed clang bug.


Solution

  • [class.mem.general]/7 specifies the complete-class contexts within a class definition: function bodies, default arguments, default template arguments, noexcept-specifiers, and default member initializers. A trailing return type in a member function signature, or the signature of a friend function defined inside the class definition, is not a complete-class context.

    However, it is not the case that a member access expression always requires the class type to be complete. In fact, [expr.ref]/4 states:

    [...] The class type shall be complete unless the class member access appears in the definition of that class.
    [Note 3: The program is ill-formed if the result differs from that when the class is complete ([class.member.lookup]). — end note]
    [Note 4: [...] — end note]

    In other words, when the type S of s is incomplete, s.foo is permitted only inside the definition of S. When a friend function is defined within the class definition, the trailing return type is inside the definition of the class, so this code should be valid.

    (Note 3 reminds us that, in effect, the lookup of foo will only find declarations that are visible at the point of lookup. If other declarations are added later that would change the result of the lookup, the program is IFNDR. Note that if the lookup is performed from a complete-class context, then the class is already complete, so it can't change even if more declarations are lexically after the point of lookup. In any case, there are no further declarations of foo in your example, so you don't have to worry about this.)

    I think this is just a bug in Clang, which seems to allow member access into S in a trailing return type of a member function of S, but not in a friend function defined within S. But a friend function defined within S still satisfies the "appears in the definition of that class" criterion, despite not being a member.