Continuing my studies in C++ concepts, I would like to define a std::tuple<t...>
data member in a concept
.
In the following code:
#include <concepts>
#include <iostream>
#include <tuple>
template <typename t>
concept c1 = requires(t p_t) {
{ p_t.i } -> std::same_as<int &>;
};
template <typename t, typename... c1>
concept c3 = requires(t p_t) {
{ p_t.cs } -> std::same_as<std::tuple<c1...> &>; // ERROR 1
};
template <c1... t_c1> struct a1 { std::tuple<t_c1...> cs; };
struct a2 {
int i{-9};
};
struct a3 {
int i{8};
};
void f(c3 auto &p_c3) { std::cout << std::get<0>(p_c3.cs).i << std::endl; } // ERROR 2
int main() {
a1<a2, a3> _a;
f(_a); // ERROR 3
return 0;
}
But there are errors:
ERROR 1
(the cause of the other errors)
main.cpp:12:17: Because type constraint 'std::same_as<std::tuple<a2, a3> &, std::tuple<> &>' was not satisfied:
main.cpp:31:3: Because type constraint 'std::same_as<std::tuple<a2, a3> &, std::tuple<> &>' was not satisfied:
ERROR 2
:
main.cpp:25:8: Because 'a1<a2, a3>' does not satisfy 'c3'
main.cpp:31:3: Because 'a1<a2, a3>' does not satisfy 'c3'
ERROR 3
:
main.cpp:31:3: No matching function for call to 'f'
main.cpp:25:6: candidate template ignored: constraints not satisfied [with p_c3:auto = a1<a2, a3>]
main.cpp:25:8: because 'a1<a2, a3>' does not satisfy 'c3'
main.cpp:12:17: because type constraint 'std::same_as<std::tuple<a2, a3> &, std::tuple<> &>' was not satisfied:
/usr/include/c++/11/concepts:63:9: note: because '__detail::__same_as<std::tuple<a2, a3> &, std::tuple<> &>' evaluated to false
/usr/include/c++/11/concepts:57:27: note: because 'std::is_same_v<std::tuple<a2, a3> &, std::tuple<> &>' evaluated to false
If I change the line { p_t.cs } -> std::same_as<std::tuple<c1...> &>;
to { p_t.cs };
the errors disappear.
So, what is the correct way to define a std::tuple
data member in a concept
?
For concepts there is no deduction of the type parameters, so given a parameter of type T
passed to f
the concept the compiler is checking is c3<T>
, but for a parameter of type a1<a2, a3>
the concept that would apply would be c3<T, a2, a3>
, i.e. the function signature would need to be
void f(c3<a2, a3> auto &p_c3);
Note that for actually implementing something like c3
, you'd need to deduce the template parameters which requires to be able to check them against c1
, so you'd need to basically go with pre-concept strategies for this:
// helper to enforce IsC1TupleHelper is only used in an unevaluated context
template<class ...Ts>
constexpr bool AlwaysFalse = false;
// helper function for deducing the template parameters of std::tuple
template<c1 ... Ts>
std::true_type IsC1TupleHelper(std::tuple<Ts...>const&)
{
static_assert(AlwaysFalse<Ts...>, "IsC1TupleHelper is only allowed to be used in an unevaluated context");
}
// helper function fallback for types other than tuples of c1
template<class T>
std::false_type IsC1TupleHelper(T const&)
{
static_assert(AlwaysFalse<T>, "IsC1TupleHelper is only allowed to be used in an unevaluated context");
}
// c3 with a better(?) name
template<class T>
concept CsMemberIsTupleOfC1 = requires(T t)
{
{ IsC1TupleHelper(t.cs) } -> std::same_as<std::true_type>;
};
void f(CsMemberIsTupleOfC1 auto& p_c3)
{
std::cout << std::get<0>(p_c3.cs).i << std::endl;
}
You need to be aware though that this concept won't prevent the user from passing a parameter of a type to f
that results in a compiler error, e.g.
a1<> _a2;
f(_a2);
fails compilation of the specialization of f
, since you try to access the first element of a tuple that doesn't have any elements using std::get<0>
.
You the following implementation of c3
would probably be more suitable and wouldn't require deducing the type parameters of the tuple:
// checks if the first element type of the tuple matches c1
template<class T>
concept FirstTupleElementIsC1 = c1<std::tuple_element_t<0, std::remove_cvref_t<T>>>;
template <typename T>
concept c3 = requires(T p_t) {
{ p_t.cs } -> FirstTupleElementIsC1;
};
or
template <typename T>
concept c3 = requires(T p_t) {
{ std::get<0>(p_t.cs) } -> c1;
};