Home > database >  Concept is satisfied by a type that would seemingly produce an invalid expression
Concept is satisfied by a type that would seemingly produce an invalid expression

Time:01-30

In the following code, the can_foo concept tests whether or not a foo() member function can be called on an instance of a type. I will use it to test instances of two templates: base conditionally enables the foo member function, and derived overrides foo to call into its base's implementation:

template <typename T>
concept can_foo = requires(T v) {
    v.foo();
};

template <bool enable_foo>
struct base {
    void foo()
        requires enable_foo
    {}
};

template <typename T>
struct derived : T {
    void foo()
    {
        static_cast<T&>(*this).foo();
    }
};

If I test whether instances of the base template satisfy the concept, it does what I would expect:

static_assert(can_foo<base<true>>); //okay
static_assert(not can_foo<base<false>>); //okay

When I wrap those types in derived, I see:

static_assert(can_foo<derived<base<true>>>); //okay
static_assert(not can_foo<derived<base<false>>>); //error: static assertion failed

This is surprising! I expected that derived<base<false>> would not satisfy can_foo - its definition of foo uses an expression that isn't valid given T = base<false>, and using the same expression tested by the concept in an evaluated context results in an error that says as much:

int main()
{
    derived<base<false>> v{};
    v.foo(); //error
}

The error message isn't at the call site, which is probably relevant; it references the body of derived<>::foo. From clang:

<source>:18:32: error: invalid reference to function 'foo': constraints not satisfied
        static_cast<T&>(*this).foo();
                               ^
<source>:31:7: note: in instantiation of member function 'derived<base<false>>::foo' requested here
    v.foo(); //"invalid reference to function 'foo'"
      ^
<source>:10:18: note: because 'false' evaluated to false
        requires enable_foo
                 ^

clang: https://godbolt.org/z/vh58TTPxo gcc: https://godbolt.org/z/qMPrzznar

Both compilers produce the same results, so I assume the problem is that there's a subtlety in the standard that I'm missing. Adding a can_foo<T> constraint to derived<T> or derived<T>::foo "fixes" this (that is, derived<base<false>> will no longer satisfy can_foo), and in a code review I would argue that this constraint should be present - but this is surprising behaviour nonetheless and I'd like to understand what's going on.

So: why does derived<false> satisfy can_foo?

CodePudding user response:

A requires-expression can only detect invalid constructs in the "immediate context" of the expression that is tested. In particular

requires(T v) {
    v.foo();
};

will not check whether it is actually well-formed to have the call v.foo(). If v.foo() would be ill-formed due to an ill-formed construct inside the body of the foo function, this will not be detected by the requires-expression because the body is not in the immediate context.

The question is, what should happen next? Should the requires-expression go and instantiate the body of foo and give a hard error, or should it return true and give you a hard error later when you attempt to call v.foo()? The answer is the second one: instantiation is not performed because it is not required. See [temp.inst]/5

Unless a function template specialization is a declared specialization, the function template specialization is implicitly instantiated when the specialization is referenced in a context that requires a function definition to exist or if the existence of the definition affects the semantics of the program. [...]

[temp.inst]/11 additionally implies that the implementation is not permitted to instantiate the definition unless it is required.

In an unevaluated context, calling v.foo() does not require the definition of foo to exist, because it is not odr-used unless it is potentially evaluated and it is normally the ODR that requires a definition to exist. (However, there are two situations where referencing a function requires its definition to exist even in an unevaluated context: when the function has a deduced return type or is needed for constant evaluation ([temp.inst]/8)). Since the definition is not required to exist, the definition is not instantiated.

You might want to modify derived::foo so that it propagates the constraint from T::foo, and thus the ill-formedness can be detected by can_foo<derived<T>>:

void foo() requires can_foo<T> {
    static_cast<T&>(*this).foo();
}

CodePudding user response:

derived has a foo for every template parameter T, it just cannot always be instantiated. When trying to instantiate derived<base<false>>::foo where you try to to call it in main, you get the error message that the instantiation is not valid since foo cannot be called for the type base<false>.

Rules regarding when instantiation is optional and/or required in the standard would be [temp.inst]. I'm however not able to point to a conclusive passage that says that checking whether derived<base<false>>::foo exists in the context of the static_assert does not require instantiation. Is it the same as overload resolution checking? "If the function selected by overload resolution can be determined without instantiating a class template definition, it is unspecified whether that instantiation actually takes place."

If you want derived::foo to only be available if it can be instantiated, you could require can_foo on its T.

template <typename T>
struct derived : T {
  void foo() requires can_foo<T>
  {
      static_cast<T&>(*this).foo();
  }
};

https://godbolt.org/z/EqrMq4Moq

  • Related