Array class that will accept an braced-init-list and deduce length

This has been asked before, but I'm curious to see if anything has changed in newer C++ standards. Any current or future standard is acceptable.

Q: Is there anyway to create an Array class that can be initialized with a braced-init-list without having to manually specify the array length, and without a 'make_array' function.

template<class T, size_t N>
struct Array
{
    T items[N];
};

Array<int> foo = { 1, 2, 3 };

Since initializer_list is not templated on the size, a constructor using it won't do the job. Deduction guides in C++17 nearly work, but you have to omit the type parameter and all items must have exactly the same type

Array foo = { 1, 2, 3 }; // Works
Array<int> foo = { 1, 2, 3 }; // Doesn't work
Array foo = { 1.0, 2.0, 3.0f }; //Doesn't work

A constructor that takes a c-array doesn't appear to work because an initializer_list won't convert to a c-array.

Is the braced-init-list to T[N] that happens in int foo[] = { 1, 2, 3 }; purely compiler magic that can't be replicated in code?

EDIT: The spirit of this question is about the exact syntax above. No make_array, no extra template argument, explicit item type, no double braces. If a trivial Array requires a bunch of modern C++ tomfoolery and still can't manage to support standard syntax then it's just a bad engineering tradeoff in my opinion.

4 answers

  • answered 2018-11-08 08:23 Steven W. Klassen

    I don't believe there is at the current time (at least as of C++17). But there may be something coming in the form of a "make_array" function. Take a look at the following. Perhaps you can adapt it to your use.

    https://en.cppreference.com/w/cpp/experimental/make_array

  • answered 2018-11-08 10:21 metalfox

    You can work around the need for all the types in the list to be the same by using an explicit deduction guide:

    template <class... T>
    Array(T&&... t) -> Array<std::common_type_t<T...>, sizeof...(T)>;
    
    Array foo = { 1.0, 2.0, 3.0f }; // Deduces Array<double,3u>
    

  • answered 2018-11-08 13:59 max66

    Deduction guides in C++17 nearly work, but you have to omit the type parameter and all items must have exactly the same type

    Not necessarily.

    Yes, you can't explicit the type parameter, but you can decide it according the types of the items.

    I imagine two reasonable strategies: (1) the type of the Array is the type of the first item, following the std::array way, so writing the following deduction guide

    template <typename T, typename ... Us>
    Array(T, Us...) -> Array<T, 1u + sizeof...(Us)>; 
    

    (but observe that a C++ program where a Us type is different from T, for a std::array, is ill formed) or (2) follows the metalfox's suggestion and select the item's common type

    template <typename ... Ts>
    Array(Ts...) -> Array<std::common_type_t<Ts...>, sizeof...(Ts)>;
    

    Is the braced-init-list to T[N] that happens in int foo[] = { 1, 2, 3 }; purely compiler magic that can't be replicated in code?

    Are you thinking in a deduction guide as follows ?

    template <typename T, std::size_t N>
    Array(T const (&)[N]) -> Array<T, N>;
    

    Works but with a couple of drawbacks: (1) you have to add a couple of brackets using it

    //    added ---V         V--- added
    Array foo1 = { { 1, 2, 3 } }; // Works
    

    and (2) remain the problem that all items must have the same type

    Array foo2 = { {1.0, 2.0, 3.0f} }; //Doesn't work: incompatible types
    

    or the compiler can't deduce the type T

    P.s.: what's wrong with a make_array() function ?

    P.s.2: I suggest to give a look at BoBTFish's answer to see a nice method to bypass the impossibility to explicit a template argument using deduction guides.

  • answered 2018-11-08 15:03 BoBTFish

    Here's one approach that allows you to specify the type. We use an extra argument to specify the type, so that we can still use the deduction guide to pick up the size. I don't consider it pretty though:

    #include <iostream>
    
    template <typename T>
    struct Tag { };
    
    template <typename T, size_t N>
    struct Array {
        T data_[N];
    
        template <typename... U>
        Array(Tag<T>, U... u)
          : data_{static_cast<T>(u)...} // cast to shut up narrowing conversions - bad idea??
        {}
    };
    
    template <typename T, typename... U>
    Array(Tag<T>, U...) -> Array<T, sizeof...(U)>;
    
    int main()
    {
        Array a{Tag<double>{}, 1, 2.0f, 3.0};
        for (auto d : a.data_) {
            std::cout << d << '\n';
        }
    }
    

    This is clearly not a full implementation of such a class, just to illustrate the technique.