Is a static member function visible inside a templated class before it is declared?

182 Views Asked by At

Should static member function b be visible to the requires-clause of the default constructor of s? What does the C++20 standard say about the legality of the provided example?

template<auto...>
struct s {
    s() requires (s::b()) = default; // clang nope, gcc ok, msvc ok
    static constexpr bool b()
    { return true; }
};
static_assert((s<>{}, true));

Live example


The error message from Clang:

<source>:3:22: error: no member named 'b' in 's<...>'
    3 |     s() requires (s::b()) = default;
      |                   ~~~^
<source>:7:15: error: static assertion expression is not an integral constant
expression
    7 | static_assert((s<>{}, true));
      |               ^~~~~~~~~~~~~
<source>:7:16: note: non-constexpr constructor 's' cannot be used in a constant
expression
    7 | static_assert((s<>{}, true));
      |                ^
<source>:3:5: note: declared here
    3 |     s() requires (s::b()) = default;
      |     ^
2

There are 2 best solutions below

3
user12002570 On

The behavior(ill-formed) of the program can be understood using expr.const#5 which states:

5. An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:

5.2 an invocation of an undefined constexpr function;

(emphasis mine)

And since at the point of the invocation of s::b(), the static constexpr function is undefined, the expression s::b() is not a constant expression at that point. Combine this with the fact that the context where the invocation is used is not a complete class context, we get the mentioned error.

A complete-class context of a class is a

  • function body ([dcl.fct.def.general]),
  • default argument
  • noexcept-specifier ([except.spec]), or
  • default member initializer

within the member-specification of the class.


Thus, the program is ill-formed.

0
Brian Bi On

In short, the member name b will not be found because b is declared after the point where it's named, and the requires-clause is not a complete-class context. [class.mem.general]/7 defines the set of complete-class contexts, and [class.member.lookup]/3 explains which class members can be found during name lookup:

The declaration set is the result of a single search in the scope of C for N from immediately after the class-specifier of C if P is in a complete-class context of C or from P otherwise. If the resulting declaration set is not empty, the subobject set contains C itself, and calculation is complete.

Since the requires-clause isn't a complete-class context, a single search is done from the point where b is named. The declaration of b doesn't precede that point, so it isn't found by the single search ([basic.lookup.general]/3).

Complicating this analysis is the fact that templates can contain dependent names, and those dependent names are subject to different rules. However, s::b is not a dependent name because its lookup context refers to the current instantiation. See [temp.dep.type]/5:

A qualified name ([basic.lookup.qual]) is dependent if

  • it is a conversion-function-id whose conversion-type-id is dependent, or
  • its lookup context is dependent and is not the current instantiation, or
  • its lookup context is the current instantiation and it is operator=, or
  • its lookup context is the current instantiation and has at least one dependent base class, and qualified name lookup for the name finds nothing ([basic.lookup.qual]).

These are basically the circumstances under which the name lookup would need to be deferred until instantiation time. In this case, because the lookup context is the current instantiation s, name lookup for b doesn't need to be deferred unless the declaration that it would find is potentially in a dependent base class (in which case you need to know the template arguments before you can know whether b is in a base class or not). In this case s::b is not a dependent name because there are no base classes. Name lookup for non-dependent names is not deferred under [temp.res.general]/1. So it finds nothing (because it's not in a complete-class context) and the program is ill-formed.

So I think GCC and MSVC have a bug here, although because name lookup in templates is such a tricky issue, it's possible that there's something I'm missing.