C++20 has landed, bringing with it Concepts. If a project were to start now and target only C++20 and later standards, would it be appropriate to say previous forms of constraints are now superseded by Concepts and the requires clause?
Are there any cases where enable_if or void_t are still required, where Concepts cannot be made to replace them?
For example, std::bit_cast is constrained to require both To and From types to be of the same size and both types to be trivially copyable.
From cppreference:
This overload participates in overload resolution only if sizeof(To) == sizeof(From) and both To and From are TriviallyCopyable types.
The MSVC standard library constrains this through an enable_if_t expression, while libcxx opts for a requires clause.
MSVC:
enable_if_t<conjunction_v<bool_constant<sizeof(_To) == sizeof(_From)>, is_trivially_copyable<_To>, is_trivially_copyable<_From>>, int> = 0
libcxx:
requires(sizeof(_ToType) == sizeof(_FromType) &&
is_trivially_copyable_v<_ToType> &&
is_trivially_copyable_v<_FromType>)
They perform the same logical operations through different language features.
To reiterate my question, are there any cases where this translation from one constraint method to another is not possible? I understand there are a lot of things Concepts and requires can do that enable_if and/or void_t can't, but can the same be said looking in the other direction? Would a wholly new codebase targeting C++20 and later ever need to fall back on these older language constructions?
I'm not aware of such cases, and indeed you could directly copy the conjunction from the MSVC example into a
requiresclause, as you can do with any compile-time boolean-testable expression. Though ideally the requirements would be captured in an actual concept:which is easier to reuse and allows you to benefit from the more expressive syntax:
This also improves the structure of the error output, which now directly mentions that your chosen types failed to satisfy the
bit_castable_toconcept, for example becauseis_trivially_copyableevaluated to false for your class Foo. However, a practical issue is that concepts can be composed of other concepts to arbitrary depths, so that the source of the failure can get buried under a long stream of messages.Other than this simpler example,
enable_ifcan be used to provide different implementations based on compile-time constraints, including different return types in the case of function overloads. This can also be done in a more readable fashion with some combination of concepts,if constexprandautoreturn type deduction (which can also be constrained by a concept).The main use of
void_tI know of is described in this question, but this too can be expressed more elegantly using concepts, for exampleYou can also make this more specific:
I think this syntax is much easier on the eyes than the tangle of
decltypeandstd::declvalthat was sometimes a necessary evil.All in all, I see no reason to continue using
enable_iforvoid_tcontraption, unless required to support older compilers. I personally would be happy to never read or write one again. Concepts are just more expressive, and because it's very common for generic C++ code to have constraints, I think having an easy way of writing at least the syntactic ones as code is a great asset.