The C++20 Concepts language feature allows one to constrain member functions of template classes. However, these constraints apply only to the body of the member functions, and not the declaration - the declaration still needs to be valid.
I'm facing a situation where the declaration may not be semantically valid, but I would like to avoid a compilation error as long as the user does not try to call that member function.
For example:
#include <vector>
template <typename T>
struct A {
// how to rewrite this declaration so that it works?
typename T::value_type f(typename T::value_type x) requires std::integral<typename T::value_type> {
return x * 10;
}
};
int main(){
A<std::vector<int>> a; // compiles
A<int> b; // does not compile, but we want this to compile as long as the user doesn't call `b.f`
struct B { using value_type = void; };
A<B> c; // does not compile, but we want this to compile as long as the user doesn't call `c.f`
}
The compilation error is:
<source>: In instantiation of 'struct A<int>':
<source>:12:12: required from here
12 | A<int> b; // does not compile
| ^
<source>:5:28: error: 'int' is not a class, struct, or union type
5 | typename T::value_type f(typename T::value_type x) requires std::integral<typename T::value_type> {
| ^
<source>: In instantiation of 'struct A<main()::B>':
<source>:14:10: required from here
14 | A<B> c; // does not compile
| ^
<source>:5:28: error: invalid parameter type 'main()::B::value_type' {aka 'void'}
5 | typename T::value_type f(typename T::value_type x) requires std::integral<typename T::value_type> {
| ^
<source>:5:28: error: in declaration 'typename T::value_type A<T>::f(typename T::value_type) requires integral<typename T::value_type>'
What is the idiomatic way to rewrite the declaration of the member function f
so that the code compiles?
I'm not sure about idiomatic, but there is a trick you can employ—have an identical template argument that doesn't get substituted until later:
template<std::same_as<T> U = T>
requires std::integral<typename T::value_type>
typename U::value_type f(typename U::value_type x) {
return x * 10;
}
The expectation is that this template argument would never be provided explicitly. It's meant to always be such that U
is the same type as T
, but since it appears possible to change U
, substitution won't happen at T
time, so the hard errors are avoided.
One could argue using T
vs. U
in the requires
, but it shouldn't make a difference in behaviour when f
is used correctly and they're the same type. requires
will handle substitution failures by failing the requirement.