9

The only and imo very inconvenient caveat of std::array is that it can't deduce its size from the initializer list like built-in C arrays, it's size must be passed as a template.

Is it possible to implement a std::array-like container (a thin wrapper around a built-in C array) with a C++11 initializer_list?

I ask because, unlike std::array, it would automatically deduce the size of the array from the initializer list which is a lot more convenient. For example:

// il_array is the hypothetical container
// automatically deduces its size from the initalizer list 
il_array <int> myarr = {2, 4, 6, 7, 8}; 

We would also want to provide a constructor to specify the size if an initializer list was not provided. For example:

// construct a fixed size array of size 10
il_array <int> myarr2 (10); 

This would also make the container more consistent with the other standard containers e.g. vector, deque and list.

To the best of my knowledge it isn't possible as the wrapped C-array e.g. T elems [size], must have constant size and initializer_list's size() member function isn't constant.

Also, I was wondering if was possible to implement such a container using a variadic template although from what I've read I don't think it's possible.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Ricky65
  • 1,657
  • 18
  • 22
  • Great, now I caused an ICE in GCC. – Kerrek SB Aug 13 '11 at 12:59
  • I tried to do this but I couldn't get it to compile. http://ideone.com/XMSeD Might be a foundation to build upon. –  Aug 13 '11 at 13:02
  • 1
    @Mike: Wait, I [already made an `make_array`](http://stackoverflow.com/questions/6114067/how-to-emulate-c-array-initialization-int-arr-e1-e2-e3-behaviour/6272491#6272491) a while ago. But it doesn't deduce from an initializer_list. – Kerrek SB Aug 13 '11 at 13:10
  • 2
    @Kerrek: Nice work. That was my first attempt at variadic templates so not suprised I couldn't get it to work. :P Back on topic, I don't think this is possible without a dynamic allocation because `initalizer_list`s member functions are not `constexpr`. –  Aug 13 '11 at 13:27
  • @Mike: I just checked, and `size()` *is* in fact constexpr. I tried to rig something up with that, but it caused a compiler crash (ICE). – Kerrek SB Aug 13 '11 at 13:30
  • @Kerrek: I must be looking at an outdated draft then. –  Aug 13 '11 at 13:39
  • @Mike: Ah, no, you're right, according to N3290 it is *not* constexpr; but in GCC 4.6.1 it is :-) I had just checked with my implementation rather than the standard... – Kerrek SB Aug 13 '11 at 13:44
  • make_array is a useful helper function but just to clarify, I'm not looking for a make_array function, rather a fixed size array container that can deduce its size from an initializer_list – Ricky65 Aug 13 '11 at 15:53
  • 1
    Even if we ignore initializer lists for the moment, with `il_array myarr2 (10); ` you are never going to get a declared array with size `10` either. – Johannes Schaub - litb Aug 13 '11 at 19:46
  • ok Johannes but do you have a working solution? – Ricky65 Aug 14 '11 at 15:57
  • Related: https://stackoverflow.com/q/6114067/23118. – hlovdal May 12 '18 at 09:47

4 Answers4

3

I think you are out of luck here. The great advantage of std::array is that it is a POD and can be statically initialized.

If you have a container with a constructor taking a std::initializer_list, it would have to copy the values (unless it is just a constant reference to the initializer, which isn't very useful).

Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • I made an implementation which took a std::initializer_list constructor and copied its contents to the C-array using std::copy. However, because this is done at run-time it means that the container has worse performance than a built-in C array, negating its usefulness. I think what is needed is a compile time equivalent to std::initializer_list. – Ricky65 Aug 14 '11 at 15:56
  • @Ricky65: `std::initializer_list` *is* a compile-time object. However, if that list contains non-POD objects, then the constructors for those objects need to be called. And that can only happen at runtime (unless they're `constexpr`, which has limits). So either way, you're going to get a bunch of constructor calls. – Nicol Bolas Dec 29 '11 at 20:17
2

Is it possible to implement a std::array-like container (a thin wrapper around a built-in C array) with a C++0x initializer_list?

Yes, well, so long as you are willing to cheat. As Mooing Duck points out, no, not even cheating, unless the compiler implementors let you. Though, it is still possible to get close enough -- it is possible to use initializer lists and a static array that is hidden by the wrapper.

This is some code I wrote for my personal toolbox. The key is to disregard the size altogether, even for the array, and let the provider container handle it; in this case, initializer_list who can provide the size via std::distance, thus avoiding client-side size explicitation (a term that I just invented, it seems).

Since it is the "anyone could've come up with it" kind of code, no problems about providing it "back" to the public; in fact, I got the idea from some expert guy whose nick I don't remember at Freenode's ##c++ channel, so I guess the recognition is for them:

*EDIT*ed:

template <typename T> struct carray {
    // typedefs for iterator. The best seems to be to use std::iterator<std::random_iterator_tag,T,int> here
    ...

    template <size_t N> 
    explicit carray (T (&arr)[N]) 
    : ax(arr), sx(N) {}

    // note the linked article. 
    // This works *only* if the compiler implementor lets you. 
    carray (std::initializer_list<T> X) 
    : ax (X.begin()), sx(std::distance(X.begin(),X.end()) {}

    // YMMV about the rest of the "rule of N":
    // no copy constructor needed -- trivial
    // no destructor needed -- data outscopes the wrapper
    // no assignment operator needed -- trivial

    // container functions, like begin(), end(), size()...

    private:
    T* ax;
    size_t const sx;
};

Usage and declaration in C++0x mode is pretty simple (just tested with GCC 4.6 in Fedora 15), but it works with the caveats noted in the external links above, so it is apparently undefined behaviour:

using lpp::carray;
carray<short const> CS = {1, -7, 4, 188};

However, I don't see why a compiler implementor would not implement an initializer_list of integrals as a static array anyway. Your call.

Not only it works like that, provided you can #ifdef the initializer constructor out of the way in pre-C++0x mode, you can actually use this in pre-C++0x; although predeclaration of the data array as its own variable will be needed, it is IMHO the closest it gets to the original intent (and it has the advantage of being usable and not causing eg.: scope issues). (also tested with the above compiler, plus Debian Wheezy's GCC):

using lpp::carray;
short data[]= {1, -7, 4, 188};
carray<short const> CS (data);

There! No "size" parameter anywhere!

We would also want to provide a constructor to specify the size if an initializer list was not provided.

Sorry, this is one feature I have not maganed to implement. The problem is how to assign the memory "statically" from an outside source, perhaps an Allocator. Assuming it could be done somehow via a helper functor allocate, then the constructor would be something like this:

explicit carray (size_t N)
: ax(allocate(N)), sx(N) {}

I hope this code is of help, as I see the question is more or less old.

Luis Machuca
  • 1,047
  • 9
  • 16
  • That's undefined behavior, `ax` points at the first element of a temporary initializer list, which goes out of scope as soon as the constructor ends. – Mooing Duck Dec 22 '11 at 22:36
  • Awww, really? Incredible I didn't catch *that* considering I had caught a couple of other possible errors. In that case even the theoretical "static allocator" wouldn't work, right? – Luis Machuca Dec 22 '11 at 23:22
  • Do we have a wording on *why* a compiler implementor would *not* implement an initializer_list as a (probably static) array in readable memory? Because that's the sensible trivial approach to take and it would solve the UB. – Luis Machuca Dec 22 '11 at 23:55
  • Even if it does, your constructor is making a temporary copy of it, and points to that. Maybe take the initializer list by reference? – Mooing Duck Dec 23 '11 at 21:10
  • Interesting, @MooingDuck, I had never thought about taking a `initializer_list` by reference. [This answer](http://stackoverflow.com/a/3425557/399580) seems to indicate it is possible and that it works that way. I'm also noticing it **does** use `.begin()` as a pointer to contiguous memory. – Luis Machuca Dec 26 '11 at 04:24
  • In C++ you normally pass objects by const reference, unless it's small or you need to modify it. Why would initializer lists be an exception? – Mooing Duck Dec 26 '11 at 05:53
  • I admire your resourcefulness, however it's not Standard Compliant so I would be wary about using it. Btw passing the initializer_list by const reference works fine on GCC 4.7. Also, instead of std::distance(X.begin(),X.end()), I would rather use X.size(). – Ricky65 Dec 26 '11 at 19:37
  • Oh right, `size()`... Anyway, thanks for the suggestion, Ricky but I'll try to see how far does this go, as it is a great opportunity to get a nifty apparently overlooked thing into the language, and "initializer_list as contiguous memory" seems the expectable thing to do anyway). It is pushing in things like this that aren't "Standard Compliant" that gave us contiguous `vector<>` memory, for example! As for Mooing's question, some silly part in me insisted in treating initializer_lists as lightweight objects like iterators, intended to be passed by value, for some reason. – Luis Machuca Dec 28 '11 at 00:02
1

How about this one? I used std::tuple instead of an initializer_list because the number of tuple arguments are available at compile-time. The tuple_array class below inherits from std::array and adds a templated constructor that is meant to be used with a std::tuple. The contents of the tuple are copied to the underlying array storage using a meta-program Assign, which simply iterates from N down to 0 at compile-time. Finally, the make_tuple_array function accepts arbitrary number of parameters and constructs a tuple_array. The type of the first argument is assumed to be the element type of the array. Good compilers should eliminate the extra copy using RVO. The program works on g++ 4.4.4 and 4.6.1 with RVO.

#include <array>
#include <tuple>
#include <iostream>

template <size_t I, typename Array, typename Tuple>
struct Assign
{
  static void execute(Array &a, Tuple const & tuple)
  {
    a[I] = std::get<I>(tuple);
    Assign<I-1, Array, Tuple>::execute(a, tuple);
  }
};

template <typename Array, typename Tuple>
struct Assign <0, Array, Tuple>
{
  static void execute(Array &a, Tuple const & tuple)
  {
    a[0] = std::get<0>(tuple);
  }
};

template <class T, size_t N>
class tuple_array : public std::array<T, N>
{
    typedef std::array<T, N> Super;

  public:

    template<typename Tuple>
    tuple_array(Tuple const & tuple)
      : Super()
    {
      Assign<std::tuple_size<Tuple>::value-1, Super, Tuple>::execute(*this, tuple);
    }
};

template <typename... Args>
tuple_array<typename std::tuple_element<0, std::tuple<Args...>>::type, sizeof...(Args)>
make_tuple_array(Args&&... args)
{
  typedef typename std::tuple_element<0, std::tuple<Args...>>::type ArgType;
  typedef tuple_array<ArgType, sizeof...(Args)> TupleArray;
  return TupleArray(std::tuple<Args...>(std::forward<Args>(args)...));
}

int main(void)
{
  auto array = make_tuple_array(10, 20, 30, 40, 50);
  for(size_t i = 0;i < array.size(); ++i)
  {
    std::cout << array[i] << " ";
  }
  std::cout << std::endl;

  return 0;
}
Sumant
  • 4,286
  • 1
  • 23
  • 31
0

I think this question is really quite simple. You need a type that will be sized to the size of an initializer_list that it is initialized with.

// il_array is the hypothetical container
// automatically deduces its size from the initalizer list 
il_array <int> myarr = {2, 4, 6, 7, 8}; 

Try this:

// il_array is the hypothetical container
// automatically deduces its size from the initalizer list 
std::initalizer_list<int> myarr = {2, 4, 6, 7, 8}; 

Does this do any copying? In the most technical sense... yes. However, copying an initializer list specifically does not copy its contents. So this costs nothing more than a couple of pointer copies. Also, any C++ compiler worth using will elide this copy into nothing.

So there you have it: an array who's size is known (via std::initializer_list::size). The limitations here are:

  1. the size is not available at compile-time.
  2. the array is not mutable.
  3. std::initializer_list is pretty bare-bones. It doesn't even have operator[].

The third is probably the most annoying. But it's also easily rectified:

template<typename E> class init_array
{
public:
  typedef std::initializer_list<E>::value_type value_type;
  typedef std::initializer_list<E>::reference reference;
  typedef std::initializer_list<E>::const_reference const_reference;
  typedef std::initializer_list<E>::size_type size_type;

  typedef std::initializer_list<E>::iterator iterator;
  typedef std::initializer_list<E>::const_iterator const_iterator;

  init_array(const std::initializer_list<E> &init_list) : m_il(init_list) {}

  init_array() noexcept {}

  size_t size() const noexcept {return m_il.size();}
  const E* begin() const noexcept {return m_il.begin();}
  const E* end() const noexcept {return m_il.end();}

  const E& operator[](size_type n) {return *(m_il.begin() + n);} 
private:
  std::initializer_list m_il;
};

There; problem solved. The initializer list constructor ensures that you can create one directly from an initializer list. And while the copy can no longer be elided, it's still just copying a pair of pointers.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982