Why does requires-expression behave differently in template and not-template for checking private member access?

374 Views Asked by At

In the following code class A has a private member function f. I want to write a static assertion that will check whether this function is accessible from the current context (as was suggested in the comment to this question).

There are 3 similar cases all based on requires-expression:

class A{ void f(); };

// #1: accepted by all
static_assert( ![](auto x){ return requires(decltype(x) a){ a.f(); }; }(A{}) );

// #2: rejected by Clang:
static_assert( ![](auto){ return requires(A a){ a.f(); }; }(nullptr) );

// #3: rejected by all
static_assert( ![](void*){ return requires(A a){ a.f(); }; }(nullptr) );

The case #3 (and #2 in Clang) is rejected with the error:

error: 'f' is a private member of 'A'

Demo: https://gcc.godbolt.org/z/Mxs4P7x8s

Why does requires-expression behave differently in template and not-template (cases #1 and #3)? Which compiler is right in #2?

1

There are 1 best solutions below

0
dfrib On BEST ANSWER

Whilst https://eel.is/c++draft/expr.prim.req.general#1 describes that

A requires-expression provides a concise way to express requirements on template arguments

the grammar of a requires-expression

requires-expression:
  requires requirement-parameter-list(opt) requirement-body

requirement-body:
  { requirement-seq }

requirement-seq:
  requirement
  requirement requirement-seq

requirement:
  simple-requirement
  [...]

simple-requirement:
  expression ;  // A simple-requirement asserts the validity of an expression.

allows requires-expressions containing arbitrary expressions that do not necessarily depend on template arguments, meaning the following is well-formed:

constexpr bool f() { return requires(int a) { a; }; }
constexpr bool g() { return requires { 0; }; }

static_assert(f());
static_assert(g());

Thus, for a requires-expression that is declared outside of a templated entity, the same rules as for any expression applies, and #3 is thus correctly rejected by all compilers (access checking control is not waived in the context of expressions in requires-expressions).

#1 is also correctly implemented by all compilers, as per https://eel.is/c++draft/expr.prim.req.general#5.sentence-2:

The substitution of template arguments into a requires-expression may result in the formation of invalid types or expressions in its requirements or the violation of the semantic constraints of those requirements. In such cases, the requires-expression evaluates to false; it does not cause the program to be ill-formed.

... as a.f(); in the constraint expression for the generic a substituted as being of type A results in an invalid expression, as f() is private and not visible.

Finally, #2 is IFNDR as per https://eel.is/c++draft/expr.prim.req.general#5.sentence-6:

If the substitution of template arguments into a requirement would always result in a substitution failure, the program is ill-formed; no diagnostic required.

We could likewise argue that it is IFNDR as per https://eel.is/c++draft/temp.res.general#6.4:

The program is ill-formed, no diagnostic required, if:

  • [...]
  • a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter, or

Thus, #2 is correctly implemented by all compilers, but it's arguably always nice when compilers actually do diagnose IFNDR violations, so a usability star for Clang.