7

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?

Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169
  • AFAIK lambda won't become available inside `decltype`, not because they're not `constexpr`, but because each and every time the lambda syntax is used a brand new type is created, which would muddle template instanciation logic a *lot*. – Quentin Sep 29 '16 at 11:27
  • @Quentin it is sad. I think it will be good, if value of any literal type can became a non-type template parameter. I sure there would be mangling issues, but instances of such a templates may be forbidden for any kind of export. In the case they are still can be useful for generic code. – Tomilov Anatoliy Sep 29 '16 at 11:30
  • I found [partial answer](http://stackoverflow.com/questions/35463646/) on my question in my previous q&a-s here (it is correct for classes w/o (even empty) bases). But what the answers to the rest part of question? – Tomilov Anatoliy Sep 29 '16 at 12:55
  • [Here](http://codereview.stackexchange.com/questions/142804/) is a possible use case. – Tomilov Anatoliy Sep 29 '16 at 13:00
  • @Quentin Whatever the committee does with lambdas in unevaluated contexts, they will not allow you to SFINAE on statements inside lambdas. That's just a giant can of worms. – T.C. Sep 29 '16 at 19:32
  • I don't think this is possible, and this is close enough to reflection that I doubt the committee will try to address it before reflection comes in. – T.C. Sep 29 '16 at 19:35
  • @T.C. I read the `2.3 Customization` section of the structured binding (I think latest) [proposal](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0144r2.pdf) and I don't know how to interpret it - does this means that there will be specialization of `std::tuple_size` for each structure that has structured bindings? – W.F. Sep 29 '16 at 19:42
  • 3
    @W.F. No, it means that if you want to allow your structure to be decomposed, and the default doesn't cut it (e.g., it doesn't have all-public data members, those data members don't come from the same class, not all of them should participate, or you want a different type for some of them), you specialize `tuple_size`/`tuple_element`/`get`. – T.C. Sep 29 '16 at 19:49

3 Answers3

9
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<std::size_t Min, std::size_t Range, template<std::size_t N>class target>
  struct maximize:
    std::conditional_t<
      maximize<Min, Range/2, target>{} == (Min+Range/2)-1,
      maximize<Min+Range/2, (Range+1)/2, target>,
      maximize<Min, Range/2, target>
    >
  {};
  template<std::size_t Min, template<std::size_t N>class target>
  struct maximize<Min, 1, target>:
    std::conditional_t<
      target<Min>{},
      std::integral_constant<std::size_t,Min>,
      std::integral_constant<std::size_t,Min-1>
    >
  {};
  template<std::size_t Min, template<std::size_t N>class target>
  struct maximize<Min, 0, target>:
    std::integral_constant<std::size_t,Min-1>
  {};

  template<class T>
  struct construct_searcher {
    template<std::size_t N>
    using result = ::can_construct_with_N<T, N>;
  };
}

template<class T, std::size_t Cap=20>
using construct_airity = details::maximize< 0, Cap, details::construct_searcher<T>::template result >;

This does a binary search for the longest construction airity of T from 0 to 20. 20 is a constant, you can increase it as you will, at compile-time and memory cost.

Live example.

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 guanteed 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 airity of it determines how many variables it needs.

Unfortunetally 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 airity. Stuff the resulting elements into a std::tuple of decltype(get<Is>(x)) and return it.

If that fails, use the above construct_airity, 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!

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    The default version of structured binding permits more than aggregates; the class at issue need only 1) has all public data members and 2) have them all be direct members of the same class. – T.C. Sep 30 '16 at 00:24
  • You may use multiplication by 2 until arity 2^n failed. Then apply binary search to higher half of 2^n range of arities. – Tomilov Anatoliy Sep 30 '16 at 04:48
  • As I mentioned above it has been already done [here](http://stackoverflow.com/questions/35463646/). Also I think much more simplier linear approach will be better for practical use (in sense of time of compilation). – Tomilov Anatoliy Sep 30 '16 at 04:49
  • This method unfortunately fails with structs that have plain arrays, because of brace elision. With `struct with_arr { int a, b, c, d[5]; };`, `construct_airity` reports 8, but you need 4 names to deconstruct it. [Proof](https://godbolt.org/z/WzYnMW) – JoaoBapt Nov 06 '20 at 22:38
  • @joao yep. Wafch for barry's http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1061r0.html proposal. – Yakk - Adam Nevraumont Nov 07 '20 at 13:28
2

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.

Live example.

Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169
  • hi, i have trouble getting this to work with derived types: struct A{}; struct B : A{int i;} a; auto& i = get<0>(a); this does not compile: " the number of identifiers must match the number of array elements or members in a structured binding declaration" do you have any idea how to get around that? – Fabian Oct 09 '17 at 22:02
  • @Fabian Maybe this works: `B b; auto & [a, i] = b; static_assert(std::is_same< decltype(a), A & >::value, "!"); static_assert(std::is_same< decltype(i), int & >::value, "!");`? – Tomilov Anatoliy Oct 10 '17 at 05:56
  • 1
    @Fabian Seems, this is impossible to achieve with `aggregate_arity` approach. There is substantial difference in number of "placeholders" between aggregate initialization and decomposition for structured bindings. – Tomilov Anatoliy Oct 10 '17 at 06:42
  • thanks for the answer, i discarded the inheritance approach and tried something else (which also fails :() please have a look: https://stackoverflow.com/questions/46675239 – Fabian Oct 10 '17 at 21:48
1

Approach taking the ideas from Yakk and applying 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);
}
Stack Danny
  • 7,754
  • 2
  • 26
  • 55