Reputation: 16741
Is it possible to determine how many variable names should I to specify in square brackets using structured bindings syntax to match the number of data members of a plain right hand side struct
?
I want to make a part of generic library, which uses structured bindings to decompose arbitrary classes into its constituents. At the moment there is no variadic version of structured bindings (and, I think, cannot be for current syntax proposed), but my first thought is to make a set of overloadings of some function decompose()
, which performs decomposition of struct
parameter into a set of its constituents. decompose()
should be overloaded by number of parameter's (which is struct
) data members. Currently constexpr if
syntax also can be used to dispatch this. But how can I emulate something similar to sizeof...
operator for above purposes? I can't use auto [a, b, c]
syntax somewhere in SFINAE constructions, because it is a decomposition declaration and AFAIK any declaration cannot be used inside decltype
, also I cannot use it for my purposes in the body of lambda functions because lambda functions cannot be used inside template arguments too.
Surely I want to have builtin operator (with syntax like sizeof[] S
/sizeof[](S)
for class S
), but something like the following is also would be acceptable:
template< typename type, typename = void >
struct sizeof_struct
{
};
template< typename type >
struct sizeof_struct< type, std::void_t< decltype([] { auto && [p1] = std::declval< type >(); void(p1); }) > >
: std::integral_constant< std::size_t, 1 >
{
};
template< typename type >
struct sizeof_struct< type, std::void_t< decltype([] { auto && [p1, p2] = std::declval< type >(); void(p1); void(p2); }) > >
: std::integral_constant< std::size_t, 2 >
{
};
... etc up to some reasonable arity
Maybe constexpr
lambda will allow us to use them into template's arguments. What do you think?
Will it be possible with coming Concepts?
Upvotes: 8
Views: 2190
Reputation: 275896
struct two_elements {
int x;
double y;
};
struct five_elements {
std::string one;
std::unique_ptr<int> two;
int * three;
char four;
std::array<two_elements, 10> five;
};
struct anything {
template<class T> operator T()const;
};
namespace details {
template<class T, class Is, class=void>
struct can_construct_with_N:std::false_type {};
template<class T, std::size_t...Is>
struct can_construct_with_N<T, std::index_sequence<Is...>,
std::void_t< decltype(T{(void(Is),anything{})...}) >>:
std::true_type
{};
}
template<class T, std::size_t N>
using can_construct_with_N =
details::can_construct_with_N<T, std::make_index_sequence<N>>;
namespace details {
template<template<size_t> class Pred>
struct maximize{
template<size_t Start, size_t Step>
struct binary_search : std::conditional<
!Step,
std::integral_constant<size_t, Start>,
typename std::conditional<
Pred<Start + Step>::value,
binary_search<Start + Step, (Step << 1)>,
binary_search<Start, (Step >> 1)>
>::type>::type {};
};
template<class T>
struct construct_searcher{
template<size_t N>
using result = can_construct_with_N<T, std::make_index_sequence<N> >;
};
}
template<class T>
using construct_arity = typename details::maximize<
details::construct_searcher<T>::template result
>::template binary_search<0, 1>;
This does a one-sided binary search for the longest construction arity of T
.
If the data in your struct cannot be constructed from an rvalue of its own type, it won't work in C++14, but I believe guaranteed elision occurs in C++17 here (!)
Turning this into structured bindings requires more than a bit of a pile of manual code. But once you have, you should be able to ask questions like "what is the 3rd type of this struct
" and the like.
If a struct
can be decomposed into structured bindings without the tuple_size
stuff being done, the arity of it determines how many variables it needs.
Unfortunately std::tuple_size
is not SFINAE friendly even in C++17. But, types that use the tuple_size
part also need to ADL-enable std::get
.
Create a namespace with a failure_tag get<std::size_t>(Ts const&...)
that using std::get
. Use that to detect if they have overridden get<0>
on the type (!std::is_same< get_type<T,0>, failure_tag >{}
), and if so go down the tuple_element
path to determine arity. Stuff the resulting elements into a std::tuple
of decltype(get<Is>(x))
and return it.
If that fails, use the above construct_arity
, and use that to figure out how to use structured bindings on the type. I'd probably then send that off into a std::tie
, for uniformity.
We now have tuple_it
which takes anything structured-binding-like and converts it to a tuple of references or values.
Now both paths have converged, and your generic code is easier!
Upvotes: 10
Reputation: 8156
Approach taking the ideas from Yakk and applying c++20 features like concepts and templated lambdas, to make it more compact:
template<std::size_t N>
struct anything {
template<class T> operator T() const;
};
template<class T, std::size_t... Ints>
concept Constructible = requires {
T{ anything<Ints>{}... };
};
template<class T, std::size_t N>
constexpr auto is_constructible() {
constexpr auto unpack = [&]<std::size_t... Ints>(std::index_sequence<Ints...>) {
return Constructible<T, Ints...>;
};
return unpack(std::make_index_sequence<N>{});
}
template<class T, std::size_t N = 0u, bool found = false>
constexpr auto find_struct_arity() {
constexpr auto constructible = is_constructible<T, N>();
if constexpr (found && !constructible) {
return N - 1;
}
else if constexpr (constructible) {
return find_struct_arity<T, N + 1, true>();
}
else {
return find_struct_arity<T, N + 1, found>();
}
}
It's a linear search for constructibility, and if it no longer works for N
braces, it returns N - 1
as a result. Example usage (Godbolt link):
struct foo2 {
int a;
double b;
};
struct foo5 {
int a;
double b;
foo2 c;
char* d;
std::array<int, 10> e;
};
int main() {
static_assert(find_struct_arity<foo2>() == 2);
static_assert(find_struct_arity<foo5>() == 5);
}
Upvotes: 2
Reputation: 16741
Also there is a linear approach to find the "aggregate arity" (though, also under the same strict enough circumsances, as in the accepted answer):
#include <type_traits>
#include <utility>
#include <tuple>
struct filler { template< typename type > operator type && (); };
template< typename aggregate,
typename index_sequence = std::index_sequence<>,
typename = void >
struct aggregate_arity
: index_sequence
{
};
template< typename aggregate,
std::size_t ...indices >
struct aggregate_arity< aggregate,
std::index_sequence< indices... >,
std::void_t< decltype(aggregate{(indices, std::declval< filler >())..., std::declval< filler >()}) > >
: aggregate_arity< aggregate,
std::index_sequence< indices..., sizeof...(indices) > >
{
};
template< std::size_t index, typename type >
constexpr
decltype(auto)
get(type & value) noexcept
{
constexpr std::size_t arity = aggregate_arity< std::remove_cv_t< type > >::size();
if constexpr (arity == 1) {
auto & [p1] = value;
if constexpr (index == 0) {
return (p1);
} else {
return;
}
} else if constexpr (arity == 2) {
auto & [p1, p2] = value;
if constexpr (index == 0) {
return (p1);
} else if constexpr (index == 1) {
return (p2);
} else {
return;
}
} else if constexpr (arity == 3) {
auto & [p1, p2, p3] = value;
if constexpr (index == 0) {
return (p1);
} else if constexpr (index == 1) {
return (p2);
} else if constexpr (index == 2) {
return (p3);
} else {
return;
}
} else /* extend it by yourself for higher arities */ {
return;
}
}
// main.cpp
#include <cstdlib>
#include <cassert>
namespace
{
using S = struct { int i; char c; bool b; };
S s{1, '2', true};
decltype(auto) i = get< 0 >(s);
decltype(auto) c = get< 1 >(s);
decltype(auto) b = get< 2 >(s);
static_assert(std::is_same< decltype(i), int & >{});
static_assert(std::is_same< decltype(c), char & >{});
static_assert(std::is_same< decltype(b), bool & >{});
static_assert(&i == &s.i);
static_assert(&c == &s.c);
static_assert(&b == &s.b);
}
int
main()
{
assert(i == 1);
assert(c == '2');
assert(b == true);
return EXIT_SUCCESS;
}
Currently argument of get()
cannot have const
top level type qualifier (i.e. type
can be &&
and &
, but not const &
and const &&
), due to the bug.
Upvotes: 3