How to emulate C array initialization "int arr[] = { e1, e2, e3, ... }" behaviour with std::array?

13,817

Solution 1

Best I can think of is:

template<class T, class... Tail>
auto make_array(T head, Tail... tail) -> std::array<T, 1 + sizeof...(Tail)>
{
     std::array<T, 1 + sizeof...(Tail)> a = { head, tail ... };
     return a;
}

auto a = make_array(1, 2, 3);

However, this requires the compiler to do NRVO, and then also skip the copy of returned value (which is also legal but not required). In practice, I would expect any C++ compiler to be able to optimize that such that it's as fast as direct initialization.

Solution 2

I'd expect a simple make_array.

template<typename ret, typename... T> std::array<ret, sizeof...(T)> make_array(T&&... refs) {
    // return std::array<ret, sizeof...(T)>{ { std::forward<T>(refs)... } };
    return { std::forward<T>(refs)... };
}

Solution 3

Combining a few ideas from previous posts, here's a solution that works even for nested constructions (tested in GCC4.6):

template <typename T, typename ...Args>
std::array<T, sizeof...(Args) + 1> make_array(T && t, Args &&... args)
{
  static_assert(all_same<T, Args...>::value, "make_array() requires all arguments to be of the same type."); // edited in
  return std::array<T, sizeof...(Args) + 1>{ std::forward<T>(t), std::forward<Args>(args)...};
}

Strangely, can cannot make the return value an rvalue reference, that would not work for nested constructions. Anyway, here's a test:

auto q = make_array(make_array(make_array(std::string("Cat1"), std::string("Dog1")), make_array(std::string("Mouse1"), std::string("Rat1"))),
                    make_array(make_array(std::string("Cat2"), std::string("Dog2")), make_array(std::string("Mouse2"), std::string("Rat2"))),
                    make_array(make_array(std::string("Cat3"), std::string("Dog3")), make_array(std::string("Mouse3"), std::string("Rat3"))),
                    make_array(make_array(std::string("Cat4"), std::string("Dog4")), make_array(std::string("Mouse4"), std::string("Rat4")))
                    );

std::cout << q << std::endl;
// produces: [[[Cat1, Dog1], [Mouse1, Rat1]], [[Cat2, Dog2], [Mouse2, Rat2]], [[Cat3, Dog3], [Mouse3, Rat3]], [[Cat4, Dog4], [Mouse4, Rat4]]]

(For the last output I'm using my pretty-printer.)


Actually, let us improve the type safety of this construction. We definitely need all types to be the same. One way is to add a static assertion, which I've edited in above. The other way is to only enable make_array when the types are the same, like so:

template <typename T, typename ...Args>
typename std::enable_if<all_same<T, Args...>::value, std::array<T, sizeof...(Args) + 1>>::type
make_array(T && t, Args &&... args)
{
  return std::array<T, sizeof...(Args) + 1> { std::forward<T>(t), std::forward<Args>(args)...};
}

Either way, you will need the variadic all_same<Args...> type trait. Here it is, generalizing from std::is_same<S, T> (note that decaying is important to allow mixing of T, T&, T const & etc.):

template <typename ...Args> struct all_same { static const bool value = false; };
template <typename S, typename T, typename ...Args> struct all_same<S, T, Args...>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value && all_same<T, Args...>::value;
};
template <typename S, typename T> struct all_same<S, T>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value;
};
template <typename T> struct all_same<T> { static const bool value = true; };

Note that make_array() returns by copy-of-temporary, which the compiler (with sufficient optimisation flags!) is allowed to treat as an rvalue or otherwise optimize away, and std::array is an aggregate type, so the compiler is free to pick the best possible construction method.

Finally, note that you cannot avoid copy/move construction when make_array sets up the initializer. So std::array<Foo,2> x{Foo(1), Foo(2)}; has no copy/move, but auto x = make_array(Foo(1), Foo(2)); has two copy/moves as the arguments are forwarded to make_array. I don't think you can improve on that, because you can't pass a variadic initializer list lexically to the helper and deduce type and size -- if the preprocessor had a sizeof... function for variadic arguments, perhaps that could be done, but not within the core language.

Solution 4

Using trailing return syntax make_array can be further simplified

#include <array>
#include <type_traits>
#include <utility>

template <typename... T>
auto make_array(T&&... t)
  -> std::array<std::common_type_t<T...>, sizeof...(t)>
{
  return {std::forward<T>(t)...};
}

int main()
{
  auto arr = make_array(1, 2, 3, 4, 5);
  return 0;
}

Unfortunatelly for aggregate classes it requires explicit type specification

/*
struct Foo
{
  int a, b;
}; */

auto arr = make_array(Foo{1, 2}, Foo{3, 4}, Foo{5, 6});

In fact this make_array implementation is listed in sizeof... operator


c++17 version

Thanks to template argument deduction for class templates proposal we can use deduction guides to get rid of make_array helper

#include <array>

namespace std
{
template <typename... T> array(T... t)
  -> array<std::common_type_t<T...>, sizeof...(t)>;
}

int main()
{
  std::array a{1, 2, 3, 4};
  return 0; 
}

Compiled with -std=c++1z flag under x86-64 gcc 7.0

Solution 5

I know it's been quite some time since this question was asked, but I feel the existing answers still have some shortcomings, so I'd like to propose my slightly modified version. Following are the points that I think some existing answers are missing.


1. No need to rely on RVO

Some answers mention that we need to rely on RVO to return the constructed array. That is not true; we can make use of copy-list-initialization to guarantee there will never be temporaries created. So instead of:

return std::array<Type, …>{values};

we should do:

return {{values}};

2. Make make_array a constexpr function

This allow us to create compile-time constant arrays.

3. No need to check that all arguments are of the same type

First off, if they are not, the compiler will issue a warning or error anyway because list-initialization doesn't allow narrowing. Secondly, even if we really decide to do our own static_assert thing (perhaps to provide better error message), we should still probably compare the arguments' decayed types rather than raw types. For example,

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array<int>(a, b, c);  // Will this work?

If we are simply static_asserting that a, b, and c have the same type, then this check will fail, but that probably isn't what we'd expect. Instead, we should compare their std::decay_t<T> types (which are all ints)).

4. Deduce the array value type by decaying the forwarded arguments

This is similar to point 3. Using the same code snippet, but don't specify the value type explicitly this time:

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array(a, b, c);  // Will this work?

We probably want to make an array<int, 3>, but the implementations in the existing answers probably all fail to do that. What we can do is, instead of returning a std::array<T, …>, return a std::array<std::decay_t<T>, …>.

There is one disadvantage about this approach: we can't return an array of cv-qualified value type any more. But most of the time, instead of something like an array<const int, …>, we would use a const array<int, …> anyway. There is a trade-off, but I think a reasonable one. The C++17 std::make_optional also takes this approach:

template< class T > 
constexpr std::optional<std::decay_t<T>> make_optional( T&& value );

Taking the above points into account, a full working implementation of make_array in C++14 looks like this:

#include <array>
#include <type_traits>
#include <utility>

template<typename T, typename... Ts>
constexpr std::array<std::decay_t<T>, 1 + sizeof... (Ts)>
make_array(T&& t, Ts&&... ts)
    noexcept(noexcept(std::is_nothrow_constructible<
                std::array<std::decay_t<T>, 1 + sizeof... (Ts)>, T&&, Ts&&...
             >::value))

{
    return {{std::forward<T>(t), std::forward<Ts>(ts)...}};
}

template<typename T>
constexpr std::array<std::decay_t<T>, 0> make_array() noexcept
{
    return {};
}

Usage:

constexpr auto arr = make_array(make_array(1, 2),
                                make_array(3, 4));
static_assert(arr[1][1] == 4, "!");
Share:
13,817
Xeo
Author by

Xeo

Game Programmer, Bookworm, Japanese Culture Fanatic, C++ Lover and Template Hacker. I'm usually found hanging out in the Lounge, where the cool kids are. Sanity is just a mask.

Updated on July 07, 2022

Comments

  • Xeo
    Xeo almost 2 years

    (Note: This question is about not having to specify the number of elements and still allow nested types to be directly initialized.)
    This question discusses the uses left for a C array like int arr[20];. On his answer, @James Kanze shows one of the last strongholds of C arrays, it's unique initialization characteristics:

    int arr[] = { 1, 3, 3, 7, 0, 4, 2, 0, 3, 1, 4, 1, 5, 9 };
    

    We don't have to specify the number of elements, hooray! Now iterate over it with the C++11 functions std::begin and std::end from <iterator> (or your own variants) and you never need to even think of its size.

    Now, are there any (possibly TMP) ways to achieve the same with std::array? Use of macros allowed to make it look nicer. :)

    ??? std_array = { "here", "be", "elements" };
    

    Edit: Intermediate version, compiled from various answers, looks like this:

    #include <array>
    #include <utility>
    
    template<class T, class... Tail, class Elem = typename std::decay<T>::type>
    std::array<Elem,1+sizeof...(Tail)> make_array(T&& head, Tail&&... values)
    {
      return { std::forward<T>(head), std::forward<Tail>(values)... };
    }
    
    // in code
    auto std_array = make_array(1,2,3,4,5);
    

    And employs all kind of cool C++11 stuff:

    • Variadic Templates
    • sizeof...
    • rvalue references
    • perfect forwarding
    • std::array, of course
    • uniform initialization
    • omitting the return type with uniform initialization
    • type inference (auto)

    And an example can be found here.

    However, as @Johannes points out in the comment on @Xaade's answer, you can't initialize nested types with such a function. Example:

    struct A{ int a; int b; };
    
    // C syntax
    A arr[] = { {1,2}, {3,4} };
    // using std::array
    ??? std_array = { {1,2}, {3,4} };
    

    Also, the number of initializers is limited to the number of function and template arguments supported by the implementation.

  • Cubbi
    Cubbi almost 13 years
    gcc 4.6.0 doesn't let the second one compile, complaining about narrowing conversion from double to value_type, but clang++ 2.9 is OK with both!
  • Matthieu M.
    Matthieu M. almost 13 years
    It's with answers like this that I understand most what Bjarne said about feeling "like a new language" :) Variadic templates, late return specifier and type deduction all-in-one!
  • Xeo
    Xeo almost 13 years
    @Matthieu: Now add rvalue refs, perfect forwarding and uniform initialization from @DeadMG's code and you've got many new features set. :>
  • Pavel Minaev
    Pavel Minaev almost 13 years
    @Cubbi: actually, g++ is right here - narrowing conversions are not permitted in aggregate initialization in C++0x (but permitted in C++03 - a breaking change I was not aware of!). I'll remove the second make_array call.
  • Pavel Minaev
    Pavel Minaev almost 13 years
    @Cubbi, yeah, but that is an explicit conversion - it would also permit silent downcasts and other such things.This can still be done by using static_assert and some TMP to detect when Tail is not implicitly convertible to T, and then using T(tail)..., but that is left as an exercise for the reader :)
  • Johannes Schaub - litb
    Johannes Schaub - litb almost 13 years
  • juanchopanza
    juanchopanza almost 13 years
    However, I think OP doesn't want to specify the size of the array, but size is a template parameter of std::array. So you need something like std::array<unsigned int, 5> n = {1,2,3,4,5};
  • Richard
    Richard almost 13 years
    std::vector<> doesn't need the explicit integer, and I'm not sure why std::array would.
  • juanchopanza
    juanchopanza almost 13 years
    @Richard, because std::vector has dynamic size, and std::array has fixed size. See this: en.wikipedia.org/wiki/Array_(C%2B%2B)
  • Richard
    Richard almost 13 years
    @juanchopanza but the {...} syntax implies compile-time constant extent, so the ctor should be able to deduce the extent.
  • Richard
    Richard almost 13 years
    I can't find the initializer_list<> spec but I suspect something like this might work: std::initializer_list<int> init_list = {1,2,3}; std::array<int, init_list.size()> my_array(init_list);
  • Assambar
    Assambar almost 13 years
    that's a good one. and a similar question about using it for assigning of nested structures can be found here Using-assign-map-list-of-for-complex-types
  • Luc Danton
    Luc Danton almost 13 years
    std::initializer_list::size is not a constexpr function and thus cannot be used like this. There are plans however from libstdc++ (the implementation shipping with GCC) to have their version constexpr.
  • Richard
    Richard almost 13 years
    @Luc any guess why it isn't constexpr?
  • Luc Danton
    Luc Danton almost 13 years
    @Richard I don't know, but my guess is that the Standard decided to be conservative when they weren't sure how easy it would be for compilers to implement that (because std::initializer_list in general needs compiler magic and can't be a library solution). In addition, if it turns out it is easy to implement, they can add it later (the reverse situation is more problematic).
  • Gabriel Garcia
    Gabriel Garcia over 9 years
    I overlooked default initialization of the std::array elements. Currently looking for a fix.
  • Gabriel Garcia
    Gabriel Garcia over 9 years
    @dyp I updated the answer with your code. If you decide to write up your own answer, let me know and I will bring mine down. Thank you.
  • dyp
    dyp over 9 years
    No, it's fine. Binding a temporary array to deduce the length is your idea, and I didn't check if my code even compiles. I think it's still your solution, and answer, with some refinement ;) One might argue though that there's no benefit to a variadic make_array as in Puppy's answer, though.
  • Gabriel Garcia
    Gabriel Garcia over 9 years
    Right. Moreover, templates cannot deduce types from initializer lists, which is one of the requirements of the question (nested braced initialization).
  • caps
    caps over 7 years
    "These don't look like good solutions." Is what I would say, too :p
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 7 years
    In C++17 a slight change to your function gives you guaranteed elision. Remove the variable a and return directly. In C++11 you can guarantee elision by doing the size computation in the return value, then doing a return {{ blah }}.
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 7 years
    Remove the std::array<ret, sizeof...(T)> on the return statement. That pointlessly forces a move constructor on the array type to exist (as opposed to a construct-from-T&&) in C++14 and C++11.
  • underscore_d
    underscore_d about 6 years
    C++17 should have a deduction guide already for this: en.cppreference.com/w/cpp/container/array/deduction_guides
  • Ciro Santilli OurBigBook.com
    Ciro Santilli OurBigBook.com over 5 years
    I love how C++ people call that simple :-)