c++c++20c++-conceptsc++23requires-clause

What is the idiomatic way to have a member function of a template class with a declaration valid only if the template argument satisfies some concept?


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?


Solution

  • 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.