【问题标题】:Container of fixed dynamic size固定动态大小的容器
【发布时间】:2013-02-15 12:47:03
【问题描述】:

是否有用于固定长度序列的标准容器,其中该长度在运行时确定。最好,我想将一个参数传递给每个序列元素的构造函数,并使用该参数来初始化一个 const 成员(或引用)。我还想在 O(1) 中的给定索引处获取序列元素。在我看来,我的所有要求都无法同时满足。

  • 我知道std::array 具有固定长度,但必须在编译时知道该长度。
  • std::vector 具有动态大小,并允许使用 emplace 传递构造函数参数。尽管您可以reserve 内存来避免实际的重新分配,但类型仍然必须是movable 才能在理论上允许这样的重新分配,例如阻止 const 成员。
  • 然后是std::liststd::forward_list,它们不需要可移动类型,但仍可调整大小,并且在随机访问模式下表现不佳。我也觉得这样的列表可能会有相当大的开销,因为每个列表节点可能会被单独分配。
  • 奇怪的是,std::valarray 是我迄今为止最好的选择,因为它有固定的长度并且不会自动调整大小。尽管有一个resize 方法,但除非您实际调用该方法,否则您的类型不必是可移动的。这里的主要缺陷是缺少自定义构造函数参数,因此无法使用这种方法初始化 const 成员。

我错过了一些替代方案吗?有什么方法可以调整其中一个标准容器以满足我的所有要求吗?


编辑:为了让您更准确地了解我正在尝试做什么,请参阅以下示例:

class A {
  void foo(unsigned n);
};

class B {
private:
  A* const a;
  const unsigned i;
public:
  B(A* aa) : a(aa), i(0) { }
  B(A* aa, unsigned ii) : a(aa), i(ii) { }
  B(const std::pair<A*, unsigned>& args) : B(args.first, args.second) { }
  B(const B&) = delete;
  B(B&&) = delete;
  B& operator=(const B&) = delete;
  B& operator=(B&&) = delete;
};

void A::foo(unsigned n) {
  // Solution using forward_list should be guaranteed to work
  std::forward_list<B> bs_list;
  for (unsigned i = n; i != 0; --i)
    bs_list.emplace_front(std::make_pair(this, i - 1));

  // Solution by Arne Mertz with single ctor argumen
  const std::vector<A*> ctor_args1(n, this);
  const std::vector<B> bs_vector(ctor_args1.begin(), ctor_args1.end());

  // Solution by Arne Mertz using intermediate creator objects
  std::vector<std::pair<A*, unsigned>> ctor_args2;
  ctor_args2.reserve(n);
  for (unsigned i = 0; i != n; ++i)
    ctor_args2.push_back(std::make_pair(this, i));
  const std::vector<B> bs_vector2(ctor_args2.begin(), ctor_args2.end());
}

【问题讨论】:

  • @leftaroundabout 不,我想他只是想要像 vector 这样永远不会重新定位其存储的东西(并且该属性必须是 static,即在编译时已知)。
  • 向量只有在增长时才会重新定位其存储空间。因此,如果您在运行时(足够早)知道需要存储多少对象,那应该不是问题。
  • @MatsPetersson 代码必须先编译。如果没有可移动类型,则无法编译对 push_back 或 emplace_back 的调用。 (如果这些路径永远不会被命中也没关系;死代码仍然需要编译)
  • @cschwan,我非常非常有兴趣看到这个在玩具示例中起作用。我看不出编译器如何在编译时确定不需要调整大小,除非它正在执行我认为不可能的级别的非常重的优化。
  • valarray&lt;T&gt; 的要求隐藏在第 26.2 节中,包括类型 T 的复制构造函数和赋值运算符。

标签: c++ c++11 containers sequence std


【解决方案1】:

理论上vector 具有您需要的属性。如您所述,如果元素不可复制和/或不可分配,则不支持可能对包含类型进行分配的操作,尤其是任何序列修改(empace_back、push_back、插入等)。因此,要创建不可复制元素的向量,您必须在向量构造期间构造每个元素。

正如 Steve Jessop 在他的回答中指出的那样,如果您首先定义向量 const,您甚至无法调用此类修改操作 - 当然元素也保持不变。

如果我理解正确,您只有一个构造函数参数序列,而不是真正的对象序列。如果它只有一个参数并且包含的​​类型有相应的构造函数,事情应该很容易:

struct C
{
  const int i_;  
  C(int i) : i_(i) {}
};

int main()
{
  const std::vector<C> theVector { 1, 2, 3, 42 };
}

如果构造函数是显式的,则必须先创建一个列表或显式构造initializer-list中的对象:

int main()
{
  auto list = { 1, 2, 3, 4 };
  const std::vector<C> theVector (std::begin(list), std::end(list));
  const std::vector<C> anotherVector { C(1), C(44) };
}

如果每个构造对象不止一个参数,请考虑使用中间创建者对象:

struct C
{
  const int i_;  
  C(int i, int y) : i_(i+y) {}
};

struct CCreator
{ 
  int i; int y; 
  explicit operator C() { return C(i,y); }
};

int main()
{
  const std::vector<CCreator> ctorArgs = { {1,2}, {3,42} };
  const std::vector<C> theVector { begin(ctorArgs), end(ctorArgs) };
}

【讨论】:

  • 这似乎不是问题的真正答案,但 AFAICS 它满足所有要求(即使所有特殊成员函数都被明确删除也有效),并且没有任何警告,例如性能开销。 +1。
  • 嗯,这有点像问题的两个部分的答案:1 他没有错过替代方案,因为矢量适合。 2 他可以通过不使用错误的方法(例如 emplace_back 等)“调整”其中一个标准容器。;-)
  • 静态初始值设定项列表不适用于我的情况,因为只有在运行时才能知道长度。但是,使用 vector 构造函数的迭代器对版本的想法对我来说效果很好,因为它会自动转换。用示例代码更新了我的问题。一个问题是我必须有一个单独的vector 用于构造函数参数,在我使用完它之后很长一段时间内它仍将保留在范围内,因此我可能会在返回完成向量的函数周围构造一些函数。或自定义序列迭代器。如果只有 C++11 有范围……
【解决方案2】:

我认为const std::vector&lt;T&gt; 具有您要求的属性。它的元素实际上并没有用const定义,但它提供了它们的常量视图。你不能改变大小。您不能调用任何需要 T 可移动的成员函数,因此对于正常使用,它们不会被实例化(如果您做了 extern 类声明,它们会被实例化,所以您不能这样做)。

如果我错了,并且您确实因为T 不可移动而遇到麻烦,请尝试使用const std::deque&lt;T&gt;

难点在于构造 blighter —— 在 C++11 中,您可以使用初始化列表来完成此操作,或者在 C++03 中,您可以从非常量向量或任何其他可以构造的 const vector获取迭代器。这并不一定意味着T 必须是可复制的,但确实需要一种可以构造它的类型(也许是您为此目的而发明的)。

【讨论】:

  • anything else you can get iterators for 部分特别有用。 Arne Mertz 对此提供了更多详细信息,但核心思想在您的两个答案中都存在,我相信一旦我将初始化丑陋隐藏在幕后某处,它就可以很好地工作。
【解决方案3】:

使用std::shared_ptr 添加间接级别。共享指针可以像往常一样被复制和分配,但不修改指向的对象。这样你应该不会有任何问题,如下例所示:

class a
{
public:
    a(int b) : b(b) { }

    // delete assignment operator
     a& operator=(a const&) = delete;

private:
    // const member
    const int b;
};

// main
std::vector<std::shared_ptr<a>> container;

container.reserve(10);
container.push_back(std::make_shared<a>(0));
container.push_back(std::make_shared<a>(1));
container.push_back(std::make_shared<a>(2));
container.push_back(std::make_shared<a>(3));

另一个优点是函数std::make_shared,它允许您使用任意数量的参数创建对象。


编辑:

正如 MvG 所说,也可以使用std::unique_ptr。使用boost::indirect_iterator,可以通过将元素复制到新向量中来删除间接:

void A::foo(unsigned n)
{
    std::vector<std::unique_ptr<B>> bs_vector;
    bs_vector.reserve(n);

    for (unsigned i = 0; i != n; ++i)
    {
        bs_vector.push_back(std::unique_ptr<B>(new B(this, i)));
    }

    typedef boost::indirect_iterator<std::vector<std::unique_ptr<B>>::iterator> it;

    // needs copy ctor for B
    const std::vector<B> bs_vector2(it(bs_vector.begin()), it(bs_vector.end()));

    // work with bs_vector2
}

【讨论】:

  • shared_ptr 在这里感觉开销很大。我想unique_ptr 应该足够了,因为我不需要可复制的东西,只需移动即可。内存管理开销应该类似于基于列表的解决方案,但访问是 O(1),所以绝对是一个胜利。这样做的一个问题是您必须在每次访问时添加一层取消引用,因此这不是对象值容器的直接替代品,而是需要修改访问该容器的所有代码。
  • MvG:我编辑了我的答案并包含了一些关于如何删除间接性的代码。
  • MvG:你为什么需要一个 const 成员?
  • 常量只是用来表达对象之间的这种关系永远不应该改变的契约。您的向量副本感觉很糟糕,因为它确实需要一个副本 ctor。但是,如果我将 vector 替换为基于它并提供这些间接迭代器来访问元素的其他东西,那么使用 boost::indirect_iterator 可能会很有用。
【解决方案4】:

我也遇到这个问题,我代码中的用例是提供一个线程安全的向量,元素个数是固定的,是原子序数。我在这里阅读了所有很棒的答案。我想我们也可以考虑我的解决方案:

只是继承了std::vector,并隐藏了push_backemplace_backerase等修饰符,那么我们就得到了一个固定大小的向量。我们只能访问和修改带有operator []的元素。

template <typename T>
class FixedVector : protected std::vector<T> {
 public:
  using BaseType = std::vector<T>;
  FixedVector(size_t n) : BaseType(n) {}
  FixedVector(const T &val, size_t n) : BaseType(val, n) {}
  typename BaseType::reference operator[](size_t n) {
    return BaseType::operator[](n);
  }
};

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-15
    • 2017-05-09
    • 1970-01-01
    • 1970-01-01
    • 2014-11-15
    • 1970-01-01
    • 2017-10-15
    相关资源
    最近更新 更多