c++templateslanguage-lawyerc++20requires-expression

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


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?


Solution

  • 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.