In my world, a StrictNodeType should be anything that defines the types PredContainer and SuccContainer, so I wrote
template<typename N>
concept StrictNodeType = requires {
typename N::PredContainer;
typename N::SuccContainer;
};
However, GCC-11.2 gives me the following error:
error: satisfaction of atomic constraint 'requires{typename N::PredContainer;typename N::SuccContainer;} [with N = typename std::remove_cvref<_Tp>::type::Node]' depends on itself
164 | concept StrictNodeType = requires {
| ^~~~~~~~~~
165 | typename N::PredContainer;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~
166 | typename N::SuccContainer;something;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
167 | };
How is that possible? Why can't GCC simply inspect the given type and check if it provides the requested subtypes?
Here's a minimum breaking example:
#include <type_traits>
template<typename N>
concept StrictNodeType = requires {
typename N::something;
};
template<StrictNodeType N> using Int = int;
template<int>
struct X {using type = Int<X>; };
using ThisBreaks = Int<X<0>>;
It seems there is an infinite recursion issue that constrains a concept more than it would be without it. I made a few changes to get more directly at the issue:
#include <type_traits>
template<typename N>
concept StrictNodeType = requires {
typename N::something;
};
#if 1
template<StrictNodeType N> using Int = int;
#else
template<typename N> using Int = int;
#endif
template<int>
struct X { using something = Int<X<0>>; };
using ThisBreaks=Int<X<0>>;
ThisBreaks foo()
{
return ThisBreaks{};
}
This yields the following error:
<source>:15:37: error: template constraint failure for 'template<class N> requires StrictNodeType<N> using Int = int'
15 | struct X { using something = Int<X<0>>; };
| ^~
<source>:15:37: note: constraints not satisfied
<source>: In substitution of 'template<class N> requires StrictNodeType<N> using Int = int [with N = X<0>]':
<source>:15:37: required from here
<source>:4:9: required for the satisfaction of 'StrictNodeType<N>' [with N = X<0>]
<source>:4:26: in requirements [with N = X<0>]
<source>:5:14: note: the required type 'typename N::something' is invalid
5 | typename N::something;
| ~~~~~~~~~^~~~~~~~~~~~~
cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail
<source>:17:25: error: template constraint failure for 'template<class N> requires StrictNodeType<N> using Int = int'
17 | using ThisBreaks=Int<X<0>>;
| ^~
<source>:17:25: note: constraints not satisfied
<source>: In substitution of 'template<class N> requires StrictNodeType<N> using Int = int [with N = X<0>]':
<source>:17:25: required from here
<source>:4:9: required for the satisfaction of 'StrictNodeType<N>' [with N = X<0>]
<source>:4:26: in requirements [with N = X<0>]
<source>:5:14: note: the required type 'typename N::something' is invalid
5 | typename N::something;
| ~~~~~~~~~^~~~~~~~~~~~~
<source>:19:1: error: 'ThisBreaks' does not name a type
19 | ThisBreaks foo()
| ^~~~~~~~~~
Compiler returned: 1
Changing #if 1 to #if 0 compiles fine, only the Concept objects to the infinite recursion.
(Play with it here: https://godbolt.org/z/56Yd7W3sf )
Related
Suppose I am writing a template function that wants to call operator+= on its template parameter and does not care whether it returns the new value (of type T), a void, or whatever else. For example:
template<class T>
void incrementAll(T *array, int size, T value) {
for (int i = 0; i < size; ++i) {
array[i] += value;
}
}
I want to have a constraint on this function: to make it require a concept on T. However, I couldn't find one that does not have any requirement on the return type. The only idea I could come up with was std::convertible_to<void> (because we can convert any value to void, right?), but it fails:
#include <concepts>
template<class T>
concept Incrementable = requires(T a, T b) {
{a += b} -> std::convertible_to<void>;
};
template<class T> requires Incrementable<T>
void incrementAll(T *array, int size, T value) {
for (int i = 0; i < size; ++i) {
array[i] += value;
}
}
int main() {
int arr[] = {1};
incrementAll(arr, 1, 1);
}
nikolay#KoLin:~$ g++ another_tmp.cpp -std=c++20 -fconcepts-diagnostics-depth=10
another_tmp.cpp: In function ‘int main()’:
another_tmp.cpp:17:21: error: no matching function for call to ‘incrementAll(int [1], int, int)’
17 | incrementAll(arr, 1, 1);
| ~~~~~~~~~~~~^~~~~~~~~~~
another_tmp.cpp:9:6: note: candidate: ‘template<class T> requires Incrementable<T> void incrementAll(T*, int, T)’
9 | void incrementAll(T *array, int size, T value) {
| ^~~~~~~~~~~~
another_tmp.cpp:9:6: note: template argument deduction/substitution failed:
another_tmp.cpp:9:6: note: constraints not satisfied
another_tmp.cpp: In substitution of ‘template<class T> requires Incrementable<T> void incrementAll(T*, int, T) [with T = int]’:
another_tmp.cpp:17:14: required from here
another_tmp.cpp:4:9: required for the satisfaction of ‘Incrementable<T>’ [with T = int]
another_tmp.cpp:4:25: in requirements with ‘T a’, ‘T b’ [with T = int]
another_tmp.cpp:5:12: note: ‘a += b’ does not satisfy return-type-requirement, because
5 | {a += b} -> std::convertible_to<void>;
| ~~^~~~
another_tmp.cpp:5:10: error: deduced expression type does not satisfy placeholder constraints
5 | {a += b} -> std::convertible_to<void>;
| ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
another_tmp.cpp:5:10: note: constraints not satisfied
In file included from another_tmp.cpp:1:
/usr/include/c++/11.2.0/concepts:72:13: required for the satisfaction of ‘convertible_to<decltype(auto) [requires std::convertible_to<<placeholder>, void>], void>’ [with decltype(auto) [requires std::convertible_to<<placeholder>, void>] = int&]
/usr/include/c++/11.2.0/concepts:72:30: note: the expression ‘is_convertible_v<_From, _To> [with _From = int&; _To = void]’ evaluated to ‘false’
72 | concept convertible_to = is_convertible_v<_From, _To>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
So, what is the correct concept to define here?
If you don't care what its return type is, then you can simply use the requires-clause to require the expression a += b to be well-formed:
template<class T>
concept Incrementable = requires(T a, T b) {
a += b;
};
However, such a concept seems too loose for the incrementable type, and it seems more appropriate to require the return type to be T& like {a += b} -> same_as<T&> and name it AdditionAssignable or something.
Consider the following code (Godbolt):
#include <iostream>
//#include <concepts>
#include <type_traits>
// Since the latest clang doesn't have <concepts>,
// took this here: https://en.cppreference.com/w/cpp/concepts/same_as
// Using of std::same_as still gives an error in GCC.
namespace detail {
template< class T, class U >
concept SameHelper = std::is_same_v<T, U>;
}
template< class T, class U >
concept same_as = detail::SameHelper<T, U> && detail::SameHelper<U, T>;
template<typename T>
concept HasStr = requires(T a) { { a.str } -> same_as<const char*>; };
struct A {
const char* str = "A";
};
const char* f(HasStr auto has_str) {
return has_str.str;
}
int main() {
A a;
std::cout << f(a) << "\n";
return 0;
}
Clang 10.0.1 successfully compiles this program. But GCC 10.2 fails:
<source>: In function 'int main()':
<source>:28:21: error: use of function 'const char* f(auto:11) [with auto:11 = A]' with unsatisfied constraints
28 | std::cout << f(a) << "\n";
| ^
<source>:22:13: note: declared here
22 | const char* f(HasStr auto has_str) {
| ^
<source>:22:13: note: constraints not satisfied
<source>: In instantiation of 'const char* f(auto:11) [with auto:11 = A]':
<source>:28:21: required from here
<source>:16:9: required for the satisfaction of 'HasStr<auto:11>' [with auto:11 = A]
<source>:16:18: in requirements with 'T a' [with T = A]
<source>:16:38: note: 'a.str' does not satisfy return-type-requirement, because
16 | concept HasStr = requires(T a) { { a.str } -> same_as<const char*>; };
| ~~^~~
<source>:16:36: error: deduced expression type does not satisfy placeholder constraints
16 | concept HasStr = requires(T a) { { a.str } -> same_as<const char*>; };
| ~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:16:36: note: constraints not satisfied
<source>:9:13: required for the satisfaction of 'SameHelper<T, U>' [with T = const char*&; U = const char*]
<source>:13:9: required for the satisfaction of 'same_as<const char*&, const char*>'
<source>:9:31: note: the expression 'is_same_v<T, U> [with T = const char*&; U = const char*]' evaluated to 'false'
9 | concept SameHelper = std::is_same_v<T, U>;
| ~~~~~^~~~~~~~~~~~~~~
Compiler returned: 1
The most interesting part is:
<source>:9:31: note: the expression 'is_same_v<T, U> [with T = const char*&; U = const char*]' evaluated to 'false'
As I understand it, in the compound requirement { a.str } expression has type const char*& instead of const char*. So, why is this happening? Which compiler is correct?
By my reading of [expr.prim.req.compound]/1, GCC is correct to emit an error:
The immediately-declared constraint ([temp.param]) of the type-constraint for decltype((E)) shall be satisfied.
With an accompanying example:
requires {
{ E1 } -> C;
{ E2 } -> D<A₁, ⋯, An>;
};
is equivalent to
requires {
E1; requires C<decltype((E1))>;
E2; requires D<decltype((E2)), A₁, ⋯, An>;
};
decltype((a.str)) is indeed const char*&, so I would expect that this is what's passed to same_as.
There is a virtual class C.
I would like to ensure that any concrete subclass inheriting from C implements a function "get" (and have a clear compile time error if one does not)
Adding a virtual "get" function to C would not work in this case, as C subclasses could implement get functions of various signatures.
(in the particular case I am working on, pybind11 will be used to creates bindings of the subclasses, and pybind11 is robust of the "get" method of B to have a wide range of signatures)
Checking at compile time if a class has a function can be done with type traits, e.g.
template<class T>
using has_get =
decltype(std::declval<T&>().get(std::declval<int>()));
My question is where in the code should I add a static assert (or smthg else) to check the existence of the "get" function. Ideally, this should be part of C declaration, as things should be easy for new user code inheriting from it. It may also be that a completely different approach would be better, which I'd like to hear.
Not sure what standard you are using but with C++20 you can do something like this using concepts
template<typename T>
concept HasGet = requires (T a)
{
a.get();
};
template<HasGet T>
void foo(T x)
{
x.get();
}
struct Foo
{
int get() {
return 1;
}
};
struct Bar
{
};
int main()
{
foo(Foo{});
foo(Bar{});
}
Error:
<source>: In function 'int main()':
<source>:27:12: error: use of function 'void foo(T) [with T = Bar]' with unsatisfied constraints
27 | foo(Bar{});
| ^
<source>:8:6: note: declared here
8 | void foo(T x)
| ^~~
<source>:8:6: note: constraints not satisfied
<source>: In instantiation of 'void foo(T) [with T = Bar]':
<source>:27:12: required from here
<source>:2:9: required for the satisfaction of 'HasGet<T>' [with T = Bar]
<source>:2:18: in requirements with 'T a' [with T = Bar]
<source>:4:9: note: the required expression 'a.get()' is invalid
4 | a.get();
EDIT:
As C++14 is preferred, if I understand you requirements, this is something you can do in C++14
#include <type_traits>
#include <utility>
using namespace std;
template<typename... Ts>
using void_t = void;
template<typename T, typename = void>
struct has_get
: false_type
{};
template<typename T>
struct has_get<T, void_t<decltype(declval<T>().get())>>
: true_type
{};
template<typename T>
static constexpr auto has_get_v = has_get<T>::value;
struct P
{
};
struct C1 : P
{
int get()
{
return 1;
}
};
struct C2 : P
{
float get()
{
return 1.0F;
}
};
struct C3
{
bool get()
{
return true;
}
};
template<typename T>
enable_if_t<is_base_of<P, decay_t<T>>::value && has_get_v<decay_t<T>>> foo(T x)
{
x.get();
}
int main()
{
foo(C1{});
foo(C2{});
foo(C3{});
}
ERROR:
<source>: In function 'int main()':
<source>:61:11: error: no matching function for call to 'foo(C3)'
61 | foo(C3{});
| ^
<source>:52:77: note: candidate: 'template<class T> std::enable_if_t<(std::is_base_of<P, typename std::decay<_Tp>::type>::value && has_get<typename std::decay<_Tp>::type>::value)> foo(T)'
52 | enable_if_t<is_base_of<P, decay_t<T>>::value && has_get<decay_t<T>>::value> foo(T x)
| ^~~
<source>:52:77: note: template argument deduction/substitution failed:
In file included from <source>:1:
/opt/compiler-explorer/gcc-10.1.0/include/c++/10.1.0/type_traits: In substitution of 'template<bool _Cond, class _Tp> using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = void]':
<source>:52:77: required by substitution of 'template<class T> std::enable_if_t<(std::is_base_of<P, typename std::decay<_Tp>::type>::value && has_get<typename std::decay<_Tp>::type>::value)> foo(T) [with T = C3]'
<source>:61:11: required from here
/opt/compiler-explorer/gcc-10.1.0/include/c++/10.1.0/type_traits:2554:11: error: no type named 'type' in 'struct std::enable_if<false, void>'
2554 | using enable_if_t = typename enable_if<_Cond, _Tp>::type;
I want to search an input range for an element that has a certain value in a member via an accessor.
range-v3 documentation is... thin, but there are sources such as this answer by AFAIK the 2 main range-v3 developers indicates this kind of thing should Just Work, albeit with sort not find.
Given the code, compiled with g++ -std=c++17 against ericniebler/range-v3 release range-v3-0.10.0, using g++.exe (Rev2, Built by MSYS2 project) 9.3.0:
#include <range/v3/algorithm/find.hpp>
#include <vector>
auto
main() -> int
{
struct S {
int i{};
auto get_i() const { return i; }
};
auto const ss = std::vector<S>(10);
ranges::find(ss, 1, &S::get_i);
return 0;
}
I get a spew of errors:
test.cpp: In function 'int main()':
test.cpp:14:31: error: no match for call to '(const ranges::find_fn) (const std::vector<main()::S>&, int, int (main()::S::*)() const)'
14 | ranges::find(ss, 1, &S::get_i);
| ^
In file included from FOO/include/range-v3/range/v3/range_fwd.hpp:24,
from FOO/include/range-v3/range/v3/algorithm/find.hpp:18,
from test.cpp:1:
FOO/include/range-v3/range/v3/detail/config.hpp:618:27: note: candidate: 'template<class I, class S, class V, class P> constexpr concepts::return_t<I, typename std::enable_if<(((input_iterator<I> && sentinel_for<S, I>) && indirect_relation<ranges::equal_to, typename ranges::detail::select_projected_<P>::apply<I>, const V*>) && concepts::detail::CPP_true(concepts::detail::Nil{})), void>::type> ranges::find_fn::operator()(I, S, const V&, P) const'
618 | #define RANGES_FUNC(NAME) operator() RANGES_FUNC_CONST_ /**/
| ^~~~~~~~
FOO/include/range-v3/range/v3/algorithm/find.hpp:47:24: note: in expansion of macro 'RANGES_FUNC'
47 | constexpr auto RANGES_FUNC(find)(I first, S last, V const & val, P proj = P{})
| ^~~~~~~~~~~
FOO/include/range-v3/range/v3/detail/config.hpp:618:27: note: template argument deduction/substitution failed:
618 | #define RANGES_FUNC(NAME) operator() RANGES_FUNC_CONST_ /**/
| ^~~~~~~~
FOO/include/range-v3/range/v3/algorithm/find.hpp:47:24: note: in expansion of macro 'RANGES_FUNC'
47 | constexpr auto RANGES_FUNC(find)(I first, S last, V const & val, P proj = P{})
| ^~~~~~~~~~~
In file included from FOO/include/range-v3/range/v3/iterator/access.hpp:22,
from FOO/include/range-v3/range/v3/iterator/concepts.hpp:30,
from FOO/include/range-v3/range/v3/algorithm/find.hpp:22,
from test.cpp:1:
FOO/include/range-v3/std/detail/associated_types.hpp: In substitution of 'template<bool B, class T> using enable_if_t = typename ranges::detail::enable_if::apply<T> [with bool B = ranges::readable<std::vector<main()::S> >; T = std::vector<main()::S>]':
FOO/include/range-v3/range/v3/iterator/concepts.hpp:561:19: required by substitution of 'template<class I> using apply = ranges::detail::enable_if_t<(bool)(readable<I>), I> [with I = std::vector<main()::S>]'
FOO/include/range-v3/range/v3/algorithm/find.hpp:48:15: required by substitution of 'template<class I, class S, class V, class P> constexpr concepts::return_t<I, typename std::enable_if<(((input_iterator<I> && sentinel_for<S, I>) && indirect_relation<ranges::equal_to, typename ranges::detail::select_projected_<P>::apply<I>, const V*>) && concepts::detail::CPP_true(concepts::detail::Nil{})), void>::type> ranges::find_fn::operator()(I, S, const V&, P) const [with I = std::vector<main()::S>; S = int; V = int (main()::S::*)() const; P = ranges::identity]'
test.cpp:14:31: required from here
FOO/include/range-v3/std/detail/associated_types.hpp:73:15: error: no class template named 'apply' in 'struct ranges::detail::enable_if<false>'
73 | using enable_if_t = typename enable_if<B>::template apply<T>;
| ^~~~~~~~~~~
In file included from FOO/include/range-v3/range/v3/range_fwd.hpp:24,
from FOO/include/range-v3/range/v3/algorithm/find.hpp:18,
from test.cpp:1:
FOO/include/range-v3/range/v3/detail/config.hpp:618:27: note: candidate: 'template<class Rng, class V, class P> constexpr concepts::return_t<typename ranges::detail::if_then<forwarding_range_<R> >::apply<decltype (ranges::_::begin(declval<Rng&>())), ranges::dangling>, typename std::enable_if<((input_range<Rng> && indirect_relation<ranges::equal_to, typename ranges::detail::select_projected_<P1>::apply<decltype (ranges::_::begin(declval<Rng&>()))>, const V*>) && concepts::detail::CPP_true(concepts::detail::Nil{})), void>::type> ranges::find_fn::operator()(Rng&&, const V&, P) const'
618 | #define RANGES_FUNC(NAME) operator() RANGES_FUNC_CONST_ /**/
| ^~~~~~~~
FOO/include/range-v3/range/v3/algorithm/find.hpp:60:24: note: in expansion of macro 'RANGES_FUNC'
60 | constexpr auto RANGES_FUNC(find)(Rng && rng, V const & val, P proj = P{})
| ^~~~~~~~~~~
FOO/include/range-v3/range/v3/detail/config.hpp:618:27: note: template argument deduction/substitution failed:
618 | #define RANGES_FUNC(NAME) operator() RANGES_FUNC_CONST_ /**/
| ^~~~~~~~
FOO/include/range-v3/range/v3/algorithm/find.hpp:60:24: note: in expansion of macro 'RANGES_FUNC'
60 | constexpr auto RANGES_FUNC(find)(Rng && rng, V const & val, P proj = P{})
| ^~~~~~~~~~~
In file included from FOO/include/range-v3/range/v3/iterator/access.hpp:22,
from FOO/include/range-v3/range/v3/iterator/concepts.hpp:30,
from FOO/include/range-v3/range/v3/algorithm/find.hpp:22,
from test.cpp:1:
FOO/include/range-v3/std/detail/associated_types.hpp: In substitution of 'template<bool B, class T> using enable_if_t = typename ranges::detail::enable_if::apply<T> [with bool B = ranges::indirectly_regular_unary_invocable<int (main()::S::*)() const, __gnu_cxx::__normal_iterator<const main()::S*, std::vector<main()::S> > >; T = ranges::detail::projected_<__gnu_cxx::__normal_iterator<const main()::S*, std::vector<main()::S> >, int (main()::S::*)() const>]':
FOO/include/range-v3/range/v3/iterator/concepts.hpp:552:19: required by substitution of 'template<class Proj> template<class I> using apply = ranges::detail::enable_if_t<(bool)(indirectly_regular_unary_invocable<Proj, I>), ranges::detail::projected_<I, Proj> > [with I = __gnu_cxx::__normal_iterator<const main()::S*, std::vector<main()::S> >; Proj = int (main()::S::*)() const]'
FOO/include/range-v3/range/v3/algorithm/find.hpp:61:15: required by substitution of 'template<class Rng, class V, class P> constexpr concepts::return_t<typename ranges::detail::if_then<forwarding_range_<R> >::apply<decltype (ranges::_::begin(declval<Rng&>())), ranges::dangling>, typename std::enable_if<((input_range<Rng> && indirect_relation<ranges::equal_to, typename ranges::detail::select_projected_<P1>::apply<decltype (ranges::_::begin(declval<Rng&>()))>, const V*>) && concepts::detail::CPP_true(concepts::detail::Nil{})), void>::type> ranges::find_fn::operator()(Rng&&, const V&, P) const [with Rng = const std::vector<main()::S>&; V = int; P = int (main()::S::*)() const]'
test.cpp:14:31: required from here
FOO/include/range-v3/std/detail/associated_types.hpp:73:15: error: no class template named 'apply' in 'struct ranges::detail::enable_if<false>'
73 | using enable_if_t = typename enable_if<B>::template apply<T>;
| ^~~~~~~~~~~
shell returned 1
Making the projection a lambda...
ranges::find( ss, 1, [](auto const& s){ return s.get_i(); } );
...works but seems wasted typing.
Referring directly to the data member...
ranges::find(ss, 1, &S::i);
...works but is not possible if it should be encapsulated behind a const getter, transformer, etc.
What am I doing wrong? Can I not use a pointer-to-member-function as projection? Is it intended?
Edit: clang++ (also on MSYS2) does work here. So I guess this must be a bug in g++. Off to Bugzilla I go... edit: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94973
This is caused by GCC on Windows defaulting to -fms-extensions for compatibility with Microsoft compilers, and that pulling in a non-standard extension that introduces ambiguity between pointers-to-member-data vs -functions written as instance.*member.
We can pass -fno-ms-extensions to stop this and compile such code succesfully, until GCC remove that particular extension, which seems unnecessary and unhelpful nowadays.
Jonathan answered this on my GCC bug 94973:
Jonathan Wakely 2020-05-06 16:26:58 UTC
Aha, the same problem happens on linux if I compile with -fms-extensions
This is the old MS extension that causes x.*f to be accepted when f is a pointer to member function, which should only be valid when used as (x.*f)().
That causes ranges::invoke to think that the projection is a pointer to data member, when actually it's a pointer to member function.
See also PR 94771 comment 4.
Jason, do we want to disable that extension in SFINAE contexts?
Jonathan Wakely 2020-05-06 16:47:42 UTC
They're on by default for mingw, for compatibility with the MS compiler (but in this case it seems the relevant extension is ancient history).
How can we make a template accept only basic data types.
template <typename T>
void GetMaxValue( T& x )
{
//... Finds max Value
}
In the above function GetMaxValue we are able to pass any value without any an error.
But the std Function std::numeric_limits<T>::max() has handled it.
For example:
auto max = std::numeric_limits< std::map<int,int> >::max();
will Give an error error C2440: '<function-style-cast>' : cannot convert from 'int' to 'std::map<_Kty,_Ty>'
With constraints in C++20:
#include <type_traits>
template <class T>
requires std::is_arithmetic_v<T>
void GetMaxValue( T& x )
{
//... Finds max Value
}
Usage:
int a = 0;
GetMaxValue(a); // fine
std::vector<int> b;
GetMaxValue(b); // compiler error
Demo
With std::enable_if otherwise:
template <class T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
void GetMaxValue( T& x )
{
//... Finds max Value
}
Demo 2
The error messages pre-constraints are harder to read:
error: no matching function for call to 'GetMaxValue(std::vector<int>&)'
| GetMaxValue(b); // compiler error
| ^
Note: candidate: 'template<class T, typename std::enable_if<is_arithmetic_v<T>, int>::type <anonymous> > void GetMaxValue(T&)'
| void GetMaxValue( T& x )
| ^~~~~~~~~~~
note: template argument deduction/substitution failed:
error: no type named 'type' in 'struct std::enable_if<false, int>'
| template <class T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
| ^
In instantiation of 'void GetMaxValue(T&) [with T = int; typename std::enable_if<is_arithmetic_v<T>, int>::type <anonymous> = 0]'
vs
error: cannot call function 'void GetMaxValue(T&) [with T = std::vector<int>]'
| GetMaxValue(b); // compiler error
| ^
note: constraints not satisfied
In function 'void GetMaxValue(T&) [with T = std::vector<int>]':
required by the constraints of 'template<class T> requires is_arithmetic_v<T> void GetMaxValue(T&)'
note: the expression 'is_arithmetic_v<T>' evaluated to 'false'
| requires std::is_arithmetic_v<T>
| ~~~~~^~~~~~~~~~~~~~~~~~