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();
}
};