4

How to define arity of an aggregate in logarithmic (at least base two) compilation time (strictly speaking, in logarithmic number of instantiations)?

What I can do currently is to achieve desired in a linear time:

#include <type_traits>
#include <utility>

struct filler { template< typename type > operator type (); };

template< typename A, typename index_sequence = std::index_sequence<>, typename = void >
struct aggregate_arity
    : index_sequence
{

};

template< typename A, std::size_t ...indices >
struct aggregate_arity< A, std::index_sequence< indices... >, std::__void_t< decltype(A{(indices, std::declval< filler >())..., std::declval< filler >()}) > >
    : aggregate_arity< A, std::index_sequence< indices..., sizeof...(indices) > >
{

};

struct A0 {};
struct A1 { double x; };
struct A2 { int i; char c; }; 
struct C50 { template< typename ...Args, typename = std::enable_if_t< (sizeof...(Args) < 51) > > C50(Args &&...) { ; } };

static_assert(aggregate_arity<  A0 >::size() == 0);
static_assert(aggregate_arity<  A1 >::size() == 1);
static_assert(aggregate_arity<  A2 >::size() == 2);
static_assert(aggregate_arity< C50 >::size() == 50);

Live example.

Please correct me if term "arity" is poor.

I think it is possible in principle: firstly one need to double arity trials starting from one until SFINAE failed (surely, in soft manner), then use bisection.

Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169

2 Answers2

5

A bit of terminology first: we can argue that you are not so much looking for the aggregate initialization arity but the maximum aggregate initialization arity. E.g. the aptly named A2 can be aggregate initialized from 0, 1, and 2 arguments so its maximum arity is 2.

Let’s turn 'is aggregate initializable from N arguments' into a trait (although with a shorter name):

struct filler { template<typename type> operator type () const; };

template<typename Arg> void accept(Arg);

template<typename Aggr, std::size_t... Indices,
         typename = decltype( accept<Aggr>({ (static_cast<void>(Indices), filler {})... }) )>
void aggregate_arity_test(std::index_sequence<Indices...>);

template<typename Aggr, int N, typename Sfinae = void>
struct has_aggregate_arity: std::false_type {};

template<typename Aggr, int N>
struct has_aggregate_arity<Aggr, N, std::void_t<decltype( aggregate_arity_test<Aggr>(std::make_index_sequence<N>()) )>>
: std::true_type {};

(We use accept<Aggr>({ args... }) because that’s the same as checking for Aggr aggr = { args... };, i.e. copy-list-initialization and what people have in mind when they talk about aggregate initialization. Aggr aggr { args.. }; is direct-list-initialization, but you can still check against that if that’s what you care about.)

Now we can find an arity for which initialization fails in not too many instantiations with iterated doubling (i.e. we will check at arity 0, then arity 1, arity 2, arity 4, arity 8, ..., arity 2i):

template<typename Aggr, int Acc = 0>
struct find_failing_init_fast: std::conditional_t<
    has_aggregate_arity<Aggr, Acc>::value,
    find_failing_init_fast<Aggr, Acc == 0 ? 1 : Acc * 2>,
    std::integral_constant<int, Acc>
> {};

Now it’s a matter of a binary search inside [0, N) where N is an arity for which initialization fails:

// binary search invariant:
//   has_aggregate_arity<Aggr, Low> && !has_aggregate_arity<Aggr, High>
template<typename Aggr, int Low, int High>
struct max_aggregate_arity_impl
: std::conditional_t<
    has_aggregate_arity<Aggr, midpoint(Low, High)>::value
      && !has_aggregate_arity<Aggr, midpoint(Low, High) + 1>::value,
    std::integral_constant<int, midpoint(Low, High)>,
    std::conditional<
        has_aggregate_arity<Aggr, midpoint(Low, High)>::value,
        max_aggregate_arity_impl<Aggr, midpoint(Low, High), High>,
        max_aggregate_arity_impl<Aggr, Low, midpoint(Low, High)>
    >
>::type {};

// special case that 'errors' out (important for SFINAE purposes)
// as the invariant obviously cannot be maintained
template<typename Aggr>
struct max_aggregate_arity_impl<Aggr, 0, 0> {};

template<typename Aggr>
struct max_aggregate_arity
: max_aggregate_arity_impl<Aggr, 0, find_failing_init_fast<Aggr>::value> {};

Live On Coliru

Luc Danton
  • 34,649
  • 6
  • 70
  • 114
  • Doubling is better than a fixed range as in my answer, +1. – davidhigh Feb 17 '16 at 19:59
  • 1
    @davidhigh In turn I really appreciated the `invariant(N) && !invariant(N+1)` test shortcut in your answer which gets rid of the need of *two* `max_aggregate_arity_impl` and `max_aggregate_arity_impl` partial specializations to handle the final cases. Esp. made worse as the latter one is not allowed and I had to resort to e.g. `max_aggregate_arity_impl>` to circumvent the restriction on non-type template parameters. – Luc Danton Feb 17 '16 at 20:07
  • Do you think that there can be anything improved by using C++17 or 20 these days? I really love your answer :) – hellow Aug 04 '23 at 07:46
2

Discussion

(The discussion is based on another answer of mine which I will delete now.)

As in the original question, the following answer checks whether the invocation of the constructor of the aggregate is possible with a given number of arguments. For aggregates, one can base a binary search on this pattern by using the following properties from the standard:

8.5.1 (6):

An initializer-list is ill-formed if the number of initializer-clauses exceeds the number of members or elements to initialize. [ Example: char cv[4] = { ’a’, ’s’, ’d’, ’f’, 0 }; // error is ill-formed. — end example ]

and

8.5.1 (7):

If there are fewer initializer-clauses in the list than there are members in the aggregate, then each member not explicitly initialized shall be initialized from its default member initializer (9.2) or, if there is no default member initializer, from an empty initializer list (8.5.4). [ Example: struct S { int a; const char* b; int c; int d = b[a]; }; S ss = { 1, "asdf" }; initializes ss.a with 1, ss.b with "asdf", ss.c with the value of an expression of the form int{} (that is, 0), and ss.d with the value of ss.b[ss.a] (that is, ’s’), and in struct X { int i, j, k = 42; }; X a[] = { 1, 2, 3, 4, 5, 6 }; X b[2] = { { 1, 2, 3 }, { 4, 5, 6 } }; a and b have the same value — end example ]

However, as you already implied by the question title, a binary search will in general not work with non-aggregates, first due to the fact that those are usually not callable with less parameters than necessary, and next due to the fact that non-aggregates can have explicit constructors so that the "conversion-to-anything" trick via the struct filler won't work.

Implementation

First ingredient is an is_callable check from here:

template<typename V, typename ... Args>
struct is_constructible_impl
{
    template<typename C> static constexpr auto test(int) -> decltype(C{std::declval<Args>() ...}, bool{}) { return true; }
    template<typename> static constexpr auto test(...) { return false; }
    static constexpr bool value = test<V>(int{});
    using type = std::integral_constant<bool, value>;
};

template<typename ... Args>
using is_constructible = typename is_callable_impl<Args...>::type;

Note that this one is usable also with a fewer number of parameters than necessary (unlike your check).

Next a helper function which takes an integer argument and returns whether the aggregate is callable with the corresponding number of constructor arguments:

template<typename A, size_t ... I>
constexpr auto check_impl(std::index_sequence<I ...>)
{
    return is_constructible<A, decltype(I, filler{}) ...>::value;
}

template<typename A, size_t N>
constexpr auto check()
{
    return check_impl<A>(std::make_index_sequence<N>{});
}

And finally the binary search:

template<typename A, size_t Low, size_t Up, size_t i = Low + (Up - Low)/2>
struct binary_search
   : public std::conditional_t<check<A, i>() && !check<A,i+1>()
                           , std::integral_constant<size_t, i>
                           , std::conditional_t<check<A, i>()
                                              , binary_search<A, i, Up>
                                              , binary_search<A, Low, i> >
                              >
{};

Use it as

int main()
{
    static_assert(binary_search<A2,0,10>::value==2);
}

Live on Coliru

Community
  • 1
  • 1
davidhigh
  • 14,652
  • 2
  • 44
  • 75
  • @Orient: you're right, I wanted to rename it as `is_constructible` ... and then came to my mind that one should better use `std::is_constructible` ... – davidhigh Feb 17 '16 at 19:42
  • 1
    [`is_braces_constructible` or `is_embraceable`](http://stackoverflow.com/questions/20885541/is-braces-constructible-type-trait), but custom `is_constructible` is good enough here too, I think. – Tomilov Anatoliy Feb 17 '16 at 19:46
  • Ok, my `std::is_constructible` trials did not succeed, probably due to [this issue](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4462.html) – davidhigh Feb 17 '16 at 19:56
  • 1
    This doesn't handle various corner cases (immovable types, non-default-constructible types, references), and handles brace elision inconsistently (elided for arrays, not for any other subaggregate). Also, surely you meant `Low + (Up - Low)/2`? – T.C. Feb 17 '16 at 23:19
  • @T.C.: `Low + (Up - Low)/2` ... of course. For the rest: do you have suggestions for improvement? Anyways, thanks for correction. – davidhigh Feb 18 '16 at 08:37