I want to define a concept
that can detect whether the type T
can be structured binding or not:
template <typename T>
concept two_elements_structured_bindable = requires (T t) {
auto [x, y] = t;
};
but this cannot be compiled. Is there a proper way to define a concept
like that?
With C++20, you can define a concept
that would identify C-style arrays and tuple-like types as structure bindable. But it cannot identify types which are being structure bindable based on public-only fields.
Possible concepts that can be implemented (see full implementation here):
template<typename T, std::size_t N>
concept structure_bindable =
(std::is_array_v<T> && (std::extent_v<T> == N)) ||
((std::tuple_size_v<T> == N) && has_get_for_size<T, N>::value);
template<typename T, typename... Ts>
concept structure_bindable_with =
structure_bindable<T, sizeof...(Ts)>
&& is_get_N<T, Ts...>(std::make_index_sequence<sizeof...(Ts)>{});
template<typename T, size_t N, typename Expected>
concept structure_bindable_with_N =
structure_bindable<T, N>
&& is_get_N<T, N-1, Expected>();
A side note: it can be achieved with compiler intrinsic features for example - here for clang (courtesy of Avi Lachmish).
My dear friend @Dvir Yitzchaki pointed out to me that based on the proposed pattern matching syntax by Herb Sutter, you can identify all stucture bindable cases, based on as
check inside a concept
, not yet in C++20, but already implemented in Circle compiler.
Herb Sutter together with the Circle compiler implementer Sean Baxter, presented the idea of the is
and as
pattern matching, as part of Herb's talk at CppCon 2021, see here.
Based on their talk, Dvir came to the idea that I later elaborated into this working implementation on Circle compiler:
template <typename T>
concept two_elements_structured_bindable = structured_bindable<T>
&& !single_element_structured_bindable<T>
&& two_elements_structured_bindable_<T>;
Based on this:
template <typename T>
concept structured_bindable = requires (T t) {
t as [...]; // note: not supported by C++20
};
template <typename T>
struct single_element_structured_bindable_wrapper {
auto first() {
auto[a, ...] = std::declval<T>(); // note: not supported by C++20
return a;
}
using first_type = decltype(first());
};
template <typename T>
concept single_element_structured_bindable = structured_bindable<T>
&& requires (T t) {
{t as [single_element_structured_bindable_wrapper<T>::first_type]};
};
And:
template <typename T>
struct two_elements_structured_bindable_wrapper {
auto first() {
auto[a, ...] = std::declval<T>(); // note: not supported by C++20
return a;
}
auto second() {
auto[a, b, ...] = std::declval<T>(); // note: not supported by C++20
return b;
}
using first_type = decltype(first());
using second_type = decltype(second());
};
template <typename T>
concept two_elements_structured_bindable_ = requires (T t) {
{t as [
two_elements_structured_bindable_wrapper<T>::first_type,
two_elements_structured_bindable_wrapper<T>::second_type
]};
};
Note that it supports checking all type of structure bindings:
static_assert(!two_elements_structured_bindable<std::tuple<int, int, int>>);
static_assert(!two_elements_structured_bindable<std::tuple<int>>);
static_assert(!two_elements_structured_bindable<int>);
static_assert(two_elements_structured_bindable<std::tuple<int, int>>);
static_assert(!two_elements_structured_bindable<std::array<int, 3>>);
static_assert(!two_elements_structured_bindable<std::array<int, 1>>);
static_assert(two_elements_structured_bindable<std::array<int, 2>>);
struct Vec3 { float x, y, z; };
static_assert(!two_elements_structured_bindable<Vec3>);
struct Vec2 { float x, y; };
static_assert(two_elements_structured_bindable<Vec2>);
After presenting above solution in a CoreCpp meetup, Dvir poured cold water on my solution, with a much shorter one:
struct anything // std::any is not good enough for that
{
template<typename T>
anything(T&&) {}
};
template<typename T>
concept twople = requires(T t)
{
t as [anything, anything];
};
I still keep the long solution above, as it has in my view some value for other implementations.
If you want to constraint on the types you would bound with, you may prefer going with another approach, which again relies on pattern matching syntax, with another proposed concept
:
template <typename T, typename... Ts>
concept TupleLike = requires (T t) {
{t as [Ts...]}; // note: not supported by C++20
};
That can allow a constraint like this:
void foo(TupleLike<double, double, double> auto tup) {
auto[a, b, c] = tup; // 3 doubles
// ...
}
Above code is again based on pattern matching syntax, not yet available in C++ (as of C++20), but already implemented in Circle compiler.
Now, in order to support a more generic concept for structured_bindable<Size>
, there is a need to use another future C++ feature, which allows sizeof...(T)
on a T
that is not a variadic pack but rather any structure bindable type. This feature might be part of p1858 or a related proposal. And again, it is already supported in Circle compiler.
This allows this very simple implementation (again, proposed by Dvir):
template <typename T, size_t SIZE>
concept has_size_of = sizeof...(T) == SIZE;
template <typename T>
concept structured_bindable = requires (T t) {
t as [...];
};
template <typename T, size_t SIZE>
concept structured_bindable_with = structured_bindable<T> && has_size_of<T, SIZE>;
Thus allowing constraining on being bindable with an exact given number, e.g.:
void foo(const structured_bindable_with<2> auto& v) {
const auto&[a, b] = v;
std::cout << a << ", " << b << std::endl;
}
In fact, if being able to provide sizeof...
becomes a feature that says you are a structure bindable type (including variadic pack by itself!), then above concept can simply become:
template <typename T, size_t SIZE>
concept structured_bindable_with = (sizeof...(T) == SIZE);
No.
There are three cases in structured bindings:
Arrays. That's easy enough to detect.
Tuple-like. You can easily check if std::tuple_size<E>::value
is valid, and then check if std::tuple_element<I, E>::type
is valid as a type for all the right types. But the get
case is harder since you have to deal with member vs non-member... but otherwise I think doable.
Types which have all public (yeah yeah, technically accessible) members as direct members of the same class. This is impossible to detect with current technology. magic_get
can, I think, handle struct X { int a, b; };
but neither struct Y : X { };
nor struct Z { X& x; };
You would need to have proper reflection to check this case.
As of C++20, you would need some kind of compiler intrinsic to do this.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With