jeudi 25 août 2016

Passing arguments to "array-like" container constructor

Background

I'm working with an embedded platform with the following restrictions:

  • No heap
  • No Boost libraries
  • C++11 is supported

I've dealt with the following problem a few times in the past:

Create an array of class type T, where T has no default constructor

The project only recently added C++11 support, and up until now I've been using ad-hoc solutions every time I had to deal with this. Now that C++11 is available, I thought I'd try to make a more general solution.

Solution Attempt

I copied an example of std::aligned_storage to come up with the framework for my array type. The result looks like this:

#include <type_traits>

template<class T, size_t N>
class Array {
  // Provide aligned storage for N objects of type T
  typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];

public:
  // Build N objects of type T in the aligned storage using default CTORs
  Array()
  {
    for(auto index = 0; index < N; ++index)
      new(data + index) T();
  }

  const T& operator[](size_t pos) const
  {
    return *reinterpret_cast<const T*>(data + pos);
  }

  // Other methods consistent with std::array API go here
};

This is a basic type - Array<T,N> only compiles if T is default-constructible. I'm not very familiar with template parameter packing, but looking at some examples led me to the following:

template<typename ...Args>
Array(Args&&... args) 
{
  for(auto index = 0; index < N; ++index)
    new(data + index) T(args...);
}

This was definitely a step in the right direction. Array<T,N> now compiles if passed arguments matching a constructor of T.

My only remaining problem is constructing an Array<T,N> where different elements in the array have different constructor arguments. I figured I could split this into two cases:

1 - User Specifies Arguments

Here's my stab at the CTOR:

template<typename U>
Array(std::initializer_list<U> initializers)
{
  // Need to handle mismatch in size between arg and array
  size_t index = 0;
  for(auto arg : initializers) {
    new(data + index) T(arg);
    index++;
  }
}

This seems to work fine, aside from needing to handle a dimension mismatch between the array and initializer list, but there are a number of ways to deal with that that aren't important. Here's an example:

struct Foo {
  explicit Foo(int i) {}
};

void bar() {
  // foos[0] == Foo(0)
  // foos[1] == Foo(1)
  // ..etc
  Array<Foo,10> foos {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
}

2 - Arguments Follow Pattern

In my previous example, foos is initialized with an incrementing list, similar to std::iota. Ideally I'd like to support something like the following, where range(int) returns SOMETHING that can initialize the array.

// One of these should initialize foos with parameters returned by range(10)
Array<Foo,10> foosA = range(10);
Array<Foo,10> foosB {range(10)};
Array<Foo,10> foosC = {range(10)};
Array<Foo,10> foosD(range(10));

Googling has shown me that std::initializer_list isn't a "normal" container, so I don't think there's any way for me to make range(int) return a std::initializer_list depending on the function parameter.

Again, there are a few options here:

  • Parameters specified at run-time (function return?)
  • Parameters specified at compile-time (constexpr function return? templates?)

Questions

  1. Are there any issues with this solution so far?
  2. Does anyone have a suggestion to generate constructor parameters? I can't think of a solution at runtime or compile-time other than hard-coding an std::initializer_list, so any ideas are welcome.

Aucun commentaire:

Enregistrer un commentaire