简体   繁体   中英

C++20: Validate Template Bodies Against Concepts

C++20 introduces concepts, which allows us to specify in the declaration of a template that the template parameters must provide certain capabilities. If a template is instantiated with a type that does not satisfy the constraints, compilation will fail at instantiation instead of while compiling the template's body and noticing an invalid expression after substitution.

This is great, but it begs the question: is there a way to have the compiler look at the template body, before instantiation (ie looking at it as a template and not a particular instantiation of a template), and check that all the expressions involving template parameters are guaranteed by the constraints to exist?

Example:

template<typename T>
concept Fooer = requires(T t)
{
  { t.foo() };
};

template<Fooer F>
void callFoo(F&& fooer)
{
  fooer.foo();
}

The concept prevents me from instantiating callFoo with a type that doesn't support the expression that's inside the template body. However, if I change the function to this:

template<Fooer F>
void callFoo(F&& fooer)
{
  fooer.foo();
  fooer.bar();
}

This will fail if I instantiate callFoo with a type that defines foo (and therefore satisfies the constraints) but not bar . In principal, the concept should enable the compiler to look at this template and reject it before instantiation because it includes the expression fooer.bar() , which is not guaranteed by the constraint to exist.

I assume there's probably backward compatibility issues with doing this, although if this validation is only done with parameters that are constrained (not just typename / class /etc. parameters), it should only affect new code.

This could be very useful because the resulting errors could be used to guide the design of constraints. Write the template implementation, compile (with no instantiations yet), then on each error, add whatever requirement is needed to the constraint. Or, in the opposite direction, when hitting an error, adjust the implementation to use only what the constraints provide.

Do any compilers support an option to enable this type of validation, or is there a plan to add this at any point? Is it part of the specification for concepts to do this validation, now or in the future?

Do any compilers support an option to enable this type of validation, or is there a plan to add this at any point? Is it part of the specification for concepts to do this validation, now or in the future?

No, no, and no.

The feature you're looking for is called definition checking . That is, the compiler checks the definition of the template at the point of its definition based on the provided concepts, and issues errors if anything doesn't validate. This is how, for instance, Rust Traits, Swift Protocols, and Haskell Typeclasses work.

But C++ concepts don't work like that, and it seems completely infeasible to ever add support for such a thing given that C++ concepts can be arbitrary expressions rather than function signatures (as they are in other languages).

The best you can do is thoroughly unit test your templates with aggressively exotic types that meet your requirements as minimally as possible (the term here is archetype ) and hope for the best.

TL;DR: no.

The design for the original C++11 concepts included validation. But when that was abandoned, the new version was designed to be much more narrow in scope. The new design was originally built on constexpr boolean conditions. The eventual requires expression was added to make these boolean checks easier to write and to bring some sanity to relationships between concepts.

But the fundamentals of the design of C++20 concepts makes it basically impossible to do full validation. Even if a concept is built entirely out of atomic requires expressions, there isn't a way to really tell if an expression is being used exactly in the code the way it is in the requires expression.

For example, consider this concept:

template<typename T, typename U>
concept func_to_u = requires(T const t)
{
  {t.func()} -> std::convertible_to<U>;
};

Now, let's imagine the following template:

template<typename T, typename U> requires func_to_u<T, U>
void foo(T const &t)
{
  std::optional<U> u(std::in_place, t.func());
}

If you look at std::optional , you find that the in_place_t constructor doesn't take a U . So... is this a legitimate use of that concept? After all, the concept says that code guarded by this concept will call func() and will convert the result to a U . But this template does not do this.

It instead takes the return type, instantiates a template that is not guarded by func_to_u , and that template does whatever it wants. Now, it turns out that this template does perform a conversion operation to U .

So on the one hand, it's clear that our code does conform to the intent of func_to_u . But that is only because it happened to pass the result to some other function that conformed to the func_to_u concept. But that template had no idea it was subject to the limitations of convertible_to<U> .

So... how is the compiler supposed to detect whether this is OK? The trigger condition for failure would be somewhere in optional 's constructor. But that constructor is not subject to the concept; it's our outer code that is subject to the concept. So the compiler would basically have to unwind every template your code uses and apply the concept to it. Only it wouldn't even be applying the whole concept; it would just be applying the convertible_to<U> part.

The complexity of doing that quickly spirals out of control.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM