8

Is there any way to use structured-binding with an arbitrary number of identities ?

P1061 "Structured Bindings can introduce a Pack" offers a convenient solution, but is not available yet.

What I want to achieve is to offer a tuple-like interface to aggregates types.
(std::get<N>(T), std::tuple_element_t<T>, etc.).

I already have a function to count fields, so what I'm looking for right now is a - even tricky way - to implement the following :

template <std::size_t N>
constexpr auto as_tuple(auto && value) noexcept {
    auto & [ /* N identities ...*/ ] = value;
    return std::tuple/* of references */{ /* N identities... */ };
}

Full of despair, I tried few ideas using preprocessors (disclaimer : not my cup of tea), with no scalable result.

// 8 bits [0..255]
#define PP_IDENTITY_BIT_1(...)   id_##__VA_ARGS__
#define PP_IDENTITY_BIT_2(...)   PP_IDENTITY_BIT_1(__VA_ARGS__ ## 0), PP_IDENTITY_BIT_1(__VA_ARGS__ ## 1)
#define PP_IDENTITY_BIT_3(...)   PP_IDENTITY_BIT_2(__VA_ARGS__ ## 0), PP_IDENTITY_BIT_2(__VA_ARGS__ ## 1)
#define PP_IDENTITY_BIT_4(...)   PP_IDENTITY_BIT_3(__VA_ARGS__ ## 0), PP_IDENTITY_BIT_3(__VA_ARGS__ ## 1)
#define PP_IDENTITY_BIT_5(...)   PP_IDENTITY_BIT_4(__VA_ARGS__ ## 0), PP_IDENTITY_BIT_4(__VA_ARGS__ ## 1)
#define PP_IDENTITY_BIT_6(...)   PP_IDENTITY_BIT_5(__VA_ARGS__ ## 0), PP_IDENTITY_BIT_5(__VA_ARGS__ ## 1)
#define PP_IDENTITY_BIT_7(...)   PP_IDENTITY_BIT_6(__VA_ARGS__ ## 0), PP_IDENTITY_BIT_6(__VA_ARGS__ ## 1)
#define PP_IDENTITY_BIT_8(...)   PP_IDENTITY_BIT_7(__VA_ARGS__ ## 0), PP_IDENTITY_BIT_7(__VA_ARGS__ ## 1)

// key idea : conditionaly expand macros, based on bitwise check,
// like `(N & 1) != 0` -> `PP_IDENTITY_BIT_1`

Playground : https://godbolt.org/z/nxaocqoKj

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Guss
  • 762
  • 4
  • 20
  • 3
    AFAIK what you want has not yet been added. It basically requires reflection, which is in the works. – NathanOliver May 17 '22 at 14:25
  • There's a boost library that was new in like boost 1.77 or one of those for doing reflection with which you can do this sort of thing but it would require "marking up" or registering the aggregate type with some kind of macro I think. it is probably written on top of one of the metaprogramming libraries in boost. https://www.boost.org/doc/libs/1_79_0/libs/describe/doc/html/describe.html – jwezorek May 17 '22 at 14:37
  • @NathanOliver You're right. But there's always a way to achieve what you wanna do - even if tricky -, that's to me the beauty of C++. – Guss May 17 '22 at 14:54
  • @jwezorek Ok I had a look to it, and in the end it's just some script that generate each tie_as_tuple specialization, up to 100 (if you're mentioning `magic_get`). I think I'll pretty much do the same if I don't find a slightly more elegant way to achieve the same behavior. – Guss May 17 '22 at 14:55
  • 6
    If there were, I wouldn't have needed to write that paper. – Barry May 17 '22 at 15:41

1 Answers1

5

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

2.1 Using Boost Preprocessor

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);
}
Turtlefight
  • 9,420
  • 2
  • 23
  • 40
  • 1
    Complete answer that matches my expectations & own research, accepted. I'll check the PP stuffs you kindly provided links about (recursive macros, `__VA_OPT__`, etc.), thx. – Guss May 18 '22 at 07:19