Implementing a std::array-like container with a C+

2019-04-24 01:54发布

问题:

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.

回答1:

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).



回答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.



回答3:

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;
}


回答4:

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.