Here I detail a MWE of what seems to be a quirk of the requires
clause used in a concept. What I want is a concept that indicates whether or not a certain function type is callable with a series of arguments. I realize that this is is provided by std::invocable
, but what I have here will illustrate the point.
Consider the following concept:
template <typename func_t, typename... args_t>
concept callable = requires(const func_t& f, const args_t&... args) {f(args...);};
This is fairly straigtforward: if I have a func_t
, can I call it with args_t...
? By my unserstanding, the concept should evaluate to true provided that calling the function with the provided arguments is a valid operation, including conversions. For example, if I have a lambda:
auto func = [](const double& i) -> void {};
Then both of the following concepts evaluate to true
:
callable<decltype(func), int> //true
callable<decltype(func), double> //true
This is seemingly because there is a conversion from int
to double
. This is fine, as this is behaviour I want in the project that made me discover this issue.
Now, I would like to call my lambda with a type that is a bit more complicated, something like the following:
auto func = [](const type1_t<space1>& t1) -> int {return 1;};
Consider the following types:
enum space {space1,space2};
template <const space sp> struct type2_t{};
template <const space sp> struct type1_t
{
type1_t(){}
template <const space sp_r>
type1_t(const type2_t<sp_r>& t2){}
};
Here we can convert type2_t
to type1_t
regardless of the template parameters, owing to the constructor template in type1_t
. Under these conditions, the following concepts evaluate to true
:
callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //true
Let's say I don't want any conversion between types that have different space
parameters. There are a couple of ways to do this, but I will choose to use a requires
clause on the type1_t
constructor:
template <const space sp_r>
requires (sp_r == sp)
type1_t(const type2_t<sp_r>& t2)
{
//all other code remains unchanged.
}
After this chance, I get the following evaluations:
callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //false
This is the behaviour I expect, as the code in the requires
clase of the concept no longer compiles.
Now, let's say that I remove the requires
clause in the constructor of type1_t
, and that the constructor now calls a member function called dummy_func
:
template <const space sp> struct type1_t
{
type1_t(){}
template <const space sp_r>
void dummy_func(const type2_t<sp_r>& t2){}
template <const space sp_r>
type1_t(const type2_t<sp_r>& t2)
{
dummy_func(t2);
}
};
The constructor remains virtually unchanged so the concepts all evaluate to true
once again:
callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //true
The strange behaviour comes when we introduce a requires
clause on dummy_func
:
template <const space sp_r>
requires (sp_r == sp)
void dummy_func(const type2_t<sp_r>& t2){}
With this clause, I expect the following concept evaluations:
callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //false
However, when I compile with the new clause, I actually get:
callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //true
This is strange to me as the following will compile:
auto func = [](const type1_t<space1>& t1) -> int {return 1;};
func(type1_t<space1>());
but this will not compile:
func(type2_t<space2>());
To me, this is contradictory with the concept callable<decltype(func), type2_t<space2>>
evaluating to true
, as I am directly using the body of code within the requires
clause.
What is the source of this contradiction? Why is the compiler not fully checking the validity of the code within the requires
clause of the concept?
Appendix
Two disclaimers:
I am aware that I should be using
std::invocable
. The above is for illustration only. Note that the same problem arises when I usestd::invocable
.I can fix the issue by placing the constraint on the constructor of
type1_t
, but this is undesireable in my project.
For the full code that shows the issue, please refer to the following:
#include <iostream>
#include <concepts>
enum space
{
space1,
space2
};
template <typename func_t, typename... args_t>
concept callable = requires(const func_t& f, const args_t&... args) {f(args...);};
template <const space sp> struct type2_t{};
template <const space sp> struct type1_t
{
type1_t(){}
template <const space sp_r>
requires (sp_r == sp)
void dummy_func(const type2_t<sp_r>& t2){}
template <const space sp_r>
type1_t(const type2_t<sp_r>& t2)
{
dummy_func(t2);
}
};
int main(int argc, char** argv)
{
auto func = [](const type1_t<space1>& t1) -> int {return 1;};
std::cout << callable<decltype(func), type1_t<space1>> << std::endl; //true
std::cout << callable<decltype(func), type2_t<space1>> << std::endl; //true
std::cout << callable<decltype(func), type2_t<space2>> << std::endl; //true, should be false!!
}
Note that I am using g 11.3 with the -std=c 20
flag.
CodePudding user response:
Constraints on functions constrain the function signature; they have nothing to do with the body of a function. Constraints don't care if the body would fail to compile; they only care about the validity of the constraints applied to that function's signature. So if you want a function to be constrained, you must put those constraints in that function's signature.
And yes, that does recursively propagate up the call graph. That's just something you have to deal with. This is one of the reasons why bundling arbitrary expression constraints into named concept
s is useful.