Unfortunately structured bindings don't support parameter packs as of C++20.
You can work around it though by providing an implementation for each different amount of arguments (like in your example)
1. Using if constexpr
You can simplify it a bit though by using if constexpr
- if the condition is not true the entire statement gets discarded (and for templated entities not instantiated as well, i.e. it doesn't have to compile for the given template type).
auto
return types only infer the return type from non-discarded return statements, so you can use if constexpr
to change the return type of functions.
This is covered by:
8.5.2 (2) The if statement
If the value of the converted condition is false, the first substatement is a discarded statement, otherwise the second substatement, if present, is a discarded statement. During the instantiation of an enclosing templated entity, if the condition is not value-dependent after its instantiation, the discarded substatement (if any) is not instantiated.
9.2.9.6.1 (3) Placeholder type specifiers
If the declared return type of the function contains a placeholder type, the return type of the function is deduced from non-discarded return statements, if any, in the body of the function.
This allows you to write your as_tuple
function like this:
Example: godbolt
#include <tuple>
#include <concepts>
template<class T>
concept aggregate = std::is_aggregate_v<T>;
struct any_type {
template<class T>
operator T() {}
};
// Count the number of members in an aggregate by (ab-)using
// aggregate initialization
template<aggregate T>
consteval std::size_t count_members(auto ...members) {
if constexpr (!requires { T{ members... }; })
return sizeof...(members) - 1;
else
return count_members<T>(members..., any_type{});
}
template<aggregate T>
constexpr auto as_tuple(T const& data) {
constexpr std::size_t fieldCount = count_members<T>();
if constexpr(fieldCount == 0) {
return std::tuple();
} else if constexpr (fieldCount == 1) {
auto& [m1] = data;
return std::tuple(m1);
} else if constexpr (fieldCount == 2) {
auto& [m1, m2] = data;
return std::tuple(m1, m2);
} else if constexpr (fieldCount == 3) {
auto& [m1, m2, m3] = data;
return std::tuple(m1, m2, m3);
} else if constexpr (fieldCount == 4) {
auto& [m1, m2, m3, m4] = data;
return std::tuple(m1, m2, m3, m4);
} else {
static_assert(fieldCount!=fieldCount, "Too many fields for as_tuple(...)! add more if statements!");
}
}
int main() {
struct toto{ int i; };
constexpr toto t{42};
constexpr auto tup = as_tuple(t);
static_assert(std::same_as<const int, std::tuple_element_t<0, decltype(tup)>>);
static_assert(42 == std::get<0>(tup));
}
Then all we need to do is generate the if constexpr
branches up to the maximum amount of members you want to support.
Note: If you want you can also replace std::tuple(...)
with std::tie(...)
to return a tuple of references to the original members instead of a tuple of copied values. (godbolt example)
2. Generating the branches
Boost Preprocessor offers a lot of convenience macros we can use to easily generate the required branches.
In this example we only need two macros from it:
BOOST_PP_ENUM_PARAMS
to generate the variable names.
Example: BOOST_PP_ENUM_PARAMS(3, foo)
would expand to foo0, foo1, foo2
BOOST_PP_REPEAT_FROM_TO
to call our macro repeately.
Example: BOOST_PP_REPEAT_FROM_TO(1, 3, FOO, ~)
would expand to FOO(z,1,~) FOO(z,2,~) FOO(z,3,~)
This allows us to generate up to 255 if constexpr
's with only a few lines of code:
Boost PP Example: godbolt
template<aggregate T>
constexpr auto as_tuple(T& data) {
constexpr std::size_t fieldCount = count_members<T>();
if constexpr(fieldCount == 0) {
return std::tuple();
}
#define AS_TUPLE_STMT(z, n, unused) \
else if constexpr(fieldCount == n) { \
auto& [ BOOST_PP_ENUM_PARAMS(n, m) ] = data; \
return std::tuple( BOOST_PP_ENUM_PARAMS(n, m) ); \
}
BOOST_PP_REPEAT_FROM_TO(1, BOOST_PP_LIMIT_REPEAT, AS_TUPLE_STMT, ~)
#undef AS_TUPLE_STMT
else {
static_assert(fieldCount!=fieldCount, "Too many fields for as_tuple(...)! add more if statements!");
}
}
This version can convert aggregates with up to 255 members into tuples.
If you need even more you can change BOOST_PP_LIMIT_MAG
& BOOST_PP_LIMIT_REPEAT
to 512 or 1024, which would allow you to handle aggregates with up to 511 or 1023 members, respectively.
2.2 Pure C++ Macros (without boost)
Without boost you'll have to manually write out a lot of boilerplate macro code - there's unfortunately no way around that.
This example uses macro "recursion" using deferred expressions and repeated scanning (the EXPAND
macros)
C++20 also added the __VA_OPT__
macro, which makes working with this a lot easier.
Example: godbolt
#define NUMBER_SEQ \
50,49,48,47,46,45,44,43,42,41, \
40,39,38,37,36,35,34,33,32,31, \
30,29,28,27,26,25,24,23,22,21, \
20,19,18,17,16,15,14,13,12,11, \
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
#define PARENS ()
#define UNWRAP(...) __VA_ARGS__
#define FIRST(el, ...) el
#define EXPAND(...) EXPAND4(EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__))))
#define EXPAND4(...) EXPAND3(EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__))))
#define EXPAND3(...) EXPAND2(EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__))))
#define EXPAND2(...) EXPAND1(EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__))))
#define EXPAND1(...) __VA_ARGS__
#define SEQ_MAP(macro, ...) __VA_OPT__(EXPAND(SEQ_MAP_HELPER(macro, __VA_ARGS__)))
#define SEQ_MAP_HELPER(macro, el, ...) macro(el __VA_OPT__(, __VA_ARGS__)) __VA_OPT__(SEQ_MAP_HELPER_AGAIN PARENS (macro, __VA_ARGS__))
#define SEQ_MAP_HELPER_AGAIN() SEQ_MAP_HELPER
#define EXPANDZ(...) EXPANDZ4(EXPANDZ4(EXPANDZ4(EXPANDZ4(__VA_ARGS__))))
#define EXPANDZ4(...) EXPANDZ3(EXPANDZ3(EXPANDZ3(EXPANDZ3(__VA_ARGS__))))
#define EXPANDZ3(...) EXPANDZ2(EXPANDZ2(EXPANDZ2(EXPANDZ2(__VA_ARGS__))))
#define EXPANDZ2(...) EXPANDZ1(EXPANDZ1(EXPANDZ1(EXPANDZ1(__VA_ARGS__))))
#define EXPANDZ1(...) __VA_ARGS__
#define SEQ_MAPZ(macro, ...) __VA_OPT__(EXPANDZ(SEQ_MAPZ_HELPER(macro, __VA_ARGS__)))
#define SEQ_MAPZ_HELPER(macro, el, ...) macro(el __VA_OPT__(, __VA_ARGS__)) __VA_OPT__(, SEQ_MAPZ_HELPER_AGAIN PARENS (macro, __VA_ARGS__))
#define SEQ_MAPZ_HELPER_AGAIN() SEQ_MAPZ_HELPER
#define ADD_M(x, ...) m##x
#define GEN_BRANCH(...) \
else if constexpr(fieldCount == FIRST(__VA_ARGS__)) { \
auto& [ SEQ_MAPZ(ADD_M, __VA_ARGS__) ] = data; \
return std::tuple( SEQ_MAPZ(ADD_M, __VA_ARGS__) ); \
}
template<aggregate T>
constexpr auto as_tuple(T& data) {
constexpr std::size_t fieldCount = count_members<T>();
if constexpr(fieldCount == 0) {
return std::tuple();
}
SEQ_MAP(GEN_BRANCH, NUMBER_SEQ)
else {
static_assert(fieldCount!=fieldCount, "Too many fields for as_tuple(...)! add more if statements!");
}
}
This version supports up to 50 members in an aggregate, but you can easily add more by adding more numbers to NUMBER_SEQ
and adding more EXPANDx
macros, as necessary (each EXPANDx
macro you add quadruples the number of scans, so you only need a few of them)
Recommended reads:
2.3 Write a code generator
Instead of using macros to generate the code you could also write a program that generates a header for your actual program as part of your build-setup.
This makes the code a lot easier to read (no macro shenanigans) - and you just have to generate it once for the number of members you want to support.
Example: godbolt
#include <iostream>
#include <vector>
int main() {
std::cout << R"(
#include <tuple>
#include <concepts>
template<class T>
concept aggregate = std::is_aggregate_v<T>;
struct any_type {
template<class T>
operator T() {}
};
template<aggregate T>
consteval std::size_t count_members(auto ...members) {
if constexpr (!requires { T{ members... }; })
return sizeof...(members) - 1;
else
return count_members<T>(members..., any_type{});
}
template<aggregate T>
constexpr auto as_tuple(T const& data) {
constexpr std::size_t fieldCount = count_members<T>();
if constexpr(fieldCount == 0) {
return std::tuple();
}
)";
std::string variables;
for(int i = 1; i <= 10; i++) {
if(variables.length() > 0) variables += ", ";
variables += "m" + std::to_string(i);
std::cout
<< " else if constexpr(fieldCount == " << i << ") {\n"
<< " auto& [" << variables << "] = data;\n"
<< " return std::tuple(" << variables << ");\n"
<< " }\n";
}
std::cout
<< " else {\n"
<< " static_assert(fieldCount!=fieldCount, \"Too many fields for as_tuple(...)! add more if statements!\");\n"
<< " }\n"
<< "}"
<< std::endl;
return 0;
}
3. Existing Implementation: Boost PFR
You might want to check out Boost PFR - it does exactly what you want to accomplish: a tuple-like interface for arbitrary structures:
Boost.PFR is a C++14 library for a very basic reflection. It gives you access to structure elements by index and provides other std::tuple like methods for user defined types without macro or boilerplate code
Example: godbolt
#include <boost/pfr.hpp>
int main() {
struct foo { char a; int b; };
constexpr foo var{'A', 42};
// converting to tuple
constexpr auto tup = boost::pfr::structure_to_tuple(var);
static_assert(std::same_as<const char, std::tuple_element_t<0, decltype(tup)>>);
static_assert(std::get<0>(tup) == 'A');
static_assert(std::get<1>(tup) == 42);
// direct
static_assert(boost::pfr::get<0>(var) == 'A');
static_assert(boost::pfr::get<1>(var) == 42);
}