【问题标题】:Passing arguments to "array-like" container constructor将参数传递给“类数组”容器构造函数
【发布时间】:2026-02-21 22:05:01
【问题描述】:

背景

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

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

我过去处理过几次以下问题:

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

该项目最近才添加了对 C++11 的支持,直到现在我每次不得不处理这个问题时都在使用临时解决方案。既然 C++11 可用,我想我会尝试做一个更通用的解决方案。

解决方案尝试

我复制了an example of 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&lt;T,N&gt; 仅在 T 是默认可构造的情况下编译。我对模板参数打包不是很熟悉,但是看了一些例子让我想到了以下几点:

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

这绝对是朝着正确方向迈出的一步。如果传递的参数与T 的构造函数匹配,则Array&lt;T,N&gt; 现在编译。

我剩下的唯一问题是构造一个Array&lt;T,N&gt;,其中数组中的不同元素具有不同的构造函数参数。我想我可以把它分成两种情况:

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) 返回可以初始化数组的东西。

// 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?它完全符合您的要求
  • 您可以将vector 与自定义分配器一起使用,将存储映射到您为此目的预留的任何存储区域
  • @GuillaumeRacicot std::array 可以处理我提到的第二种初始化类型吗?在我的例子中,我可以去:std::array&lt;Foo,3&gt; = { Foo(0), Foo(1), Foo(2) }; 不过我不知道如何扩展它。如果我将一个函数返回{ Foo(0), Foo(1), Foo(2) } 作为std::initializer_list&lt;Foo&gt; 并尝试使用它来初始化std::array,我会收到错误消息。我可以使用std::array 而不对所有元素进行硬编码吗?如果Foo 不可复制怎么办?
  • @M.M 是的,我认为这些方面的东西会起作用。我不太喜欢这个,因为容器应该像一个数组而不是像一个向量。如果容器是具有“永久”(相对于容器的生命周期)元素的固定大小,我宁愿不公开 std::vector API。
  • @Matt std::array 不要使用std::initializer_list,而是聚合初始化,这是不同的东西。例如,您可以拥有std::array&lt;std::unique_ptr&lt;int&gt;, 2&gt; myArray{std::make_unique&lt;int&gt;(), std::make_unique&lt;int&gt;()},它会正确构建。

标签: c++ arrays c++11 memory-management initializer-list


【解决方案1】:

如果我正确理解您的问题,我还偶然发现 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]);

具体的实现示例有点大的代码,所以如果您想了解除一般概念之外的更多实现细节,请告诉我 - 我会更新我的答案。

【讨论】:

    【解决方案2】:

    我会使用工厂 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)

    【讨论】: