向“类似数组”的容器构造函数传递参数

4

背景

我正在使用一个具有以下限制的嵌入式平台:

  • 没有堆内存
  • 没有 Boost 库
  • 支持 C++11

我以前遇到过以下问题:

创建一个类类型 T 的数组,其中 T 没有默认构造函数

直到最近项目才添加了 C++11 支持,之前每次处理这个问题时都是使用 ad-hoc 解决方案。现在可以使用 C++11,所以我想尝试制定一个更通用的解决方案。

解决方案尝试

我复制了 std::aligned_storage 示例 来设计我的数组类型框架。结果如下:

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

这是一个基本类型 - Array<T,N> 只有在T是默认可构造的情况下才会编译。我对模板参数打包不是很熟悉,但是查看一些示例后,我得出了以下结论:

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

这绝对是朝着正确方向迈出的一步。Array<T,N>现在可以编译通过,如果传递的参数与T的构造函数匹配。
我唯一剩下的问题是如何构造一个Array<T,N>,其中数组中的不同元素具有不同的构造函数参数。我想把它分成两种情况:
1-用户指定参数
以下是我的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++;
  }
}

这看起来运行良好,除了需要处理数组和初始化列表之间的维度不匹配问题外,但有许多方法可以解决这个问题,这些并不重要。以下是一个示例:

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 - 参数遵循模式

在我之前的例子中,foos 被初始化为递增列表,类似于 std::iota。理想情况下,我希望支持以下方式,其中 range(int) 返回 SOMETHING 可以初始化数组。

// 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));

通过搜索,我发现 std::initializer_list 不是一个“正常”的容器,因此我认为没有任何方法使 range(int) 根据函数参数返回一个 std::initializer_list

同样,这里有几个选项:

  • 在运行时指定参数(函数返回值?)
  • 在编译时指定参数(constexpr 函数返回?模板?)

问题

  1. 迄今为止,这个解决方案是否存在任何问题?
  2. 是否有人有建议来生成构造函数参数?除了硬编码一个 std::initializer_list,我想不到运行时或编译时的解决方案,所以欢迎任何想法。

为什么不使用std:array呢?它完全符合您的要求。 - Guillaume Racicot
你可以使用一个定制的分配器将存储映射到为此目的设置的任何存储区域,并与vector一起使用。 - M.M
@GuillaumeRacicot 我想问一下,std::array 能处理我提到的第二种初始化类型吗?在我的例子中,我可以这样写:std::array<Foo,3> = { Foo(0), Foo(1), Foo(2) };但我不知道如何扩展它。如果我编写一个返回 { Foo(0), Foo(1), Foo(2) } 作为 std::initializer_list<Foo> 的函数,并尝试使用它来初始化一个 std::array,我会得到一个错误。我能否在不硬编码所有元素的情况下使用 std::array?如果 Foo 不可复制怎么办? - Matt K
@M.M 是的,我想到了类似的解决方案。但我不是很喜欢这个方法,因为容器应该像数组而不是向量一样运作。如果容器的元素数量是固定的,并且相对于容器的生命周期来说是“永久”的,那么我宁愿不暴露 std::vector 的 API。 - Matt K
@Matt std::array 不使用 std::initializer_list,而是使用聚合初始化,这是两个不同的概念。例如,您可以拥有 std::array<std::unique_ptr<int>, 2> myArray{std::make_unique<int>(), std::make_unique<int>()} 并且它将正确构建。 - Guillaume Racicot
@GuillaumeRacicot 我意识到两者之间的区别 - 我认为我面临的唯一问题是因为 std::array 使用聚合初始化。在你的例子中,myArray 起作用是因为 std::make_unique<int> 的返回类型与 std::array<std::unique_ptr<int>,2> 的元素类型匹配。这对 std::array<T,N> 有一些限制:
  1. T 必须可移动或可复制
  2. 初始化列表不能由函数返回
我认为这些问题没有解决方法。如果有的话,我会同意 std::array 可以胜任这项工作。
- Matt K
2个回答

0

如果我正确理解了您的问题,我也曾遇到过 std::array 在元素构造方面完全不灵活,而更倾向于聚合初始化(以及缺乏具有灵活元素构造选项的静态分配容器)。我想到的最好方法是创建一个自定义的类似数组的容器,它接受迭代器来构造其元素。 这是完全灵活的解决方案:

  • 适用于固定大小和动态大小的容器
  • 可以将不同或相同的参数传递给元素构造函数
  • 可以使用一个或多个(元组分段构造)参数调用构造函数,甚至为不同的元素调用不同的构造函数(控制反转)

对于您的示例,它将是这样的:

const size_t SIZE = 10;

std::array<int, SIZE> params;
for (size_t c = 0; c < SIZE; c++) {
    params[c] = c;
}

Array<Foo, SIZE> foos(iterator_construct, &params[0]); //iterator_construct is a special tag to call specific constructor
// also, we are able to pass a pointer as iterator, since it has both increment and dereference operators

注意:你可以通过使用自定义迭代器类来跳过参数数组分配,该类将根据其位置实时计算其值。

对于多参数构造函数,可以这样实现:

const size_t SIZE = 10;

std::array<std::tuple<int, float>, SIZE> params; // will call Foo(int, float)
for (size_t c = 0; c < SIZE; c++) {
    params[c] = std::make_tuple(c, 1.0f);
}

Array<Foo, SIZE> foos(iterator_construct, piecewise_construct, &params[0]);

具体实现示例是一个很长的代码片段,如果您希望了解更多关于实现细节方面的洞见,请告诉我 - 我会更新我的答案。


0

我会使用工厂lambda。

该lambda接受一个指向构造位置的指针和一个索引,并负责构造。

这也使得复制/移动操作更容易编写,这是一个好兆头。

template<class T, std::size_t N>
struct my_array {
  T* data() { return (T*)&buffer; }
  T const* data() const { return (T const*)&buffer; }

  // basic random-access container operations:
  T* begin() { return data(); }
  T const* begin() const { return data(); }
  T* end() { return data()+N; }
  T const* end() const { return data()+N; }
  T& operator[](std::size_t i){ return *(begin()+i); }
  T const& operator[](std::size_t i)const{ return *(begin()+i); }

  // useful utility:
  bool empty() const { return N!=0; }
  T& front() { return *begin(); }
  T const& front() const { return *begin(); }
  T& back() { return *(end()-1); }
  T const& back() const { return *(end()-1); }
  std::size_t size() const { return N; }

  // construct from function object:
  template<class Factory,
    typename std::enable_if<!std::is_same<std::decay_t<Factory>, my_array>::value, int> =0
  >
  my_array( Factory&& factory ) {
    std::size_t i = 0;
    try {
      for(; i < N; ++i) {
        factory( (void*)(data()+i), i );
      }
    } catch(...) {
      // throw during construction.  Unroll creation, and rethrow:
      for(std::size_t j = 0; j < i; ++j) {
        (data()+i-j-1)->~T();
      }
      throw;
    }
  }
  // other constructors, in terms of above naturally:
  my_array():
    my_array( [](void* ptr, std::size_t) {
      new(ptr) T();
    } )
  {}
  my_array(my_array&& o):
    my_array( [&](void* ptr, std::size_t i) {
      new(ptr) T( std::move(o[i]) );
    } )
  {}
  my_array(my_array const& o):
    my_array( [&](void* ptr, std::size_t i) {
      new(ptr) T( o[i] );
    } )
  {}
  my_array& operator=(my_array&& o) {
    for (std::size_t i = 0; i < N; ++i)
      (*this)[i] = std::move(o[i]);
    return *this;
  }
  my_array& operator=(my_array const& o) {
    for (std::size_t i = 0; i < N; ++i)
      (*this)[i] = o[i];
    return *this;
  }
private:
  using storage = typename std::aligned_storage< sizeof(T)*N, alignof(T) >::type;
  storage buffer;
};

它定义了my_array(),但只有在尝试编译时才会被编译。

支持初始化器列表相对容易。当 il 不够长或太长时决定该怎么做则很困难。我认为你可能需要:

template<class Fail>
my_array( std::initializer_list<T> il, Fail&& fail ):
  my_array( [&](void* ptr, std::size_t i) {
    if (i < il.size()) new(ptr) T(il[i]);
    else fail(ptr, i);
  } )
{}

这需要您传递一个“失败时该执行什么操作”的参数。我们可以通过添加以下内容来默认抛出异常:

template<class WhatToThrow>
struct throw_when_called {
  template<class...Args>
  void operator()(Args&&...)const {
    throw WhatToThrow{"when called"};
  }
};

struct list_too_short:std::length_error {
  list_too_short():std::length_error("list too short") {}
};
template<class Fail=ThrowWhenCalled<list_too_short>>
my_array( std::initializer_list<T> il, Fail&& fail={} ):
  my_array( [&](void* ptr, std::size_t i) {
    if (i < il.size()) new(ptr) T(il[i]);
    else fail(ptr, i);
  } )
{}

如果我写得正确,这将使一个太短的初始化列表引起一个有意义的抛出消息。在您的平台上,如果您没有异常,您可以只需使用exit(-1)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接