A use-case for member concepts and/or template concept parameters
I continue, very slowly, to work on my CppCon 2018 session “Concepts As She Is Spoke.” A while back, someone on the cpplang Slack asked whether it was possible to have either “template concept parameters” or “member concepts” — that is, either
template<
template<class> concept TemplateConceptParameter
>
struct Foo {
template<class T>
void bar() requires TemplateConceptParameter<T> {
// ...
}
};
Foo<Integral> foo;
foo.bar(1); // OK
foo.bar("no"); // error
or the poor man’s version,
struct integral_helper {
template<class T>
concept MemberConcept = Integral<T>;
};
template<class X>
struct Foo {
template<class T>
void bar() requires X::template MemberConcept<T> {
// ...
}
};
Foo<integral_helper> foo;
foo.bar(1); // OK
foo.bar("no"); // error
They were asked for a concrete use-case. Well, I’ve got a concrete use-case now, albeit kind of ivory-tower.
I want to write a little snippet of C++ code to illustrate the C++2a notion of “subsumption” —
how one concept can refine another concept in a way understandable by the compiler.
The rules for subsumption are quite persnickety: sometimes the compiler understands
that concept A
subsumes B
, and sometimes it doesn’t.
For any given pair of concepts A
and B
, we can test their relationship to each
other by creating a pair of functions, one constrained on A
and the other constrained
on B
, and then seeing whether that makes overload resolution happy or sad.
template<class T>
concept AlwaysTrue =
std::is_void_v<std::void_t<T>>;
template<class T>
concept EvenMoreTrue = AlwaysTrue<T> &&
std::is_void_v<std::void_t<T, void>>;
template<class Subsumes, class = int>
struct helper : std::false_type {};
template<class S>
struct helper<S, decltype(S::template f<void>())>
: std::true_type {};
template<
template<class> concept A
template<class> concept B
>
struct Subsumes {
template<class T> static void f()
requires B<T> || AlwaysTrue<T>;
template<class T> static int f()
requires A<T> || EvenMoreTrue<T>;
using type = typename helper<Subsumes>::type;
static constexpr bool value = type::value;
};
static_assert(Subsumes<Integral, Scalar>);
static_assert(not Subsumes<Scalar, Integral>);
This works because if A
subsumes B
, then A<T> || EvenMoreTrue<T>
will subsume B<T> || AlwaysTrue<T>
,
and incidentally both constraints will always be satisfied, even for T=void
, which is how we’re going
to instantiate f()
.
So this snippet works great to verify that subsumption works the way I expect it to. The only problem is that we can’t literally write
template<
template<class> concept A
template<class> concept B
>
Instead, we have to fall back on Boost-era macro metaprogramming:
#define DEFINE_SUBSUMES(A,B) \
struct Subsumes##A##B { \
template<class T> static void f() \
requires B<T> || AlwaysTrue<T>; \
template<class T> static int f() \
requires A<T> || EvenMoreTrue<T>; \
using type = typename helper<Subsumes##A##B>::type; \
static constexpr bool value = type::value; \
};
DEFINE_SUBSUMES(Scalar, Integral)
DEFINE_SUBSUMES(Integral, Scalar)
DEFINE_SUBSUMES(NonScalar, NonIntegral)
DEFINE_SUBSUMES(NonIntegral, NonScalar)
static_assert(SubsumesIntegralScalar::value);
static_assert(not SubsumesScalarIntegral::value);
static_assert(not SubsumesNonScalarNonIntegral::value);
static_assert(not SubsumesNonIntegralNonScalar::value);
Is this a motivating enough example to get member concepts into C++2a? Or merely motivating enough to get subsumption out? Honestly I’d accept either one as a step forward.