【问题标题】:Resources management - vector and pointers资源管理 - 向量和指针
【发布时间】:2015-02-03 23:17:05
【问题描述】:

我需要存储ThirdPartyElm 类型的元素序列,并且我正在使用std::vector(如果我需要固定大小的序列,则使用std::array)。

我想知道我应该如何初始化序列。第一个版本创建一个新元素,并且(如果我是对的)在将元素插入序列时创建一个副本:

for (int i = 0; i < N; i++)
{
   auto elm = ThirdPartyElm();
   // init elm..
   my_vector.push_back(elm);  // my_array[i] = elm;
}

第二个版本存储一个指针序列(或者更好的c++11智能指针):

for (int i = 0; i < N; i++)
{
   std::unique_ptr<ThirdPartyElm> elm(new ThirdPartyElm());
   // init elm..
   my_vector.push_back(std::move(elm));  // my_array[i] = std::move(elm);
}

哪个版本最轻量级?

请突出显示所有错误。

【问题讨论】:

  • 你能定义“轻量级”吗?第二个版本有这样的优势,如果你愿意为 ThirdPartyElm 定义一个删除函数对象ThirdPartyElmDeleter,那么你可以通过 std::vector&lt;std::unique_ptr&lt;ThirdPartyElm,ThirdPartyElmDeleter&gt; &gt; 来实现 pimpl idiom。
  • 不要把新的智能指针看成指针,尤其是在“轻量级”方面(智能指针方式实际上使用更多内存,因为你需要空间对于实际的智能指针对象及其指针),大多数时候您应该从所有权的角度查看std::unique_ptrstd::shared_ptr

标签: c++ pointers c++11 vector initialization


【解决方案1】:

你可以只声明它的大小,它会调用这些元素的默认构造函数。

std::vector<ThirdPartyElem> my_vector(N);

就你的陈述而言

第一个版本创建一个新元素,并且(如果我是对的)在将元素插入到序列中时创建该元素的副本

别担心。由于ele 是一个即将超出范围的局部变量,因此您的编译器可能会使用copy elision,以便调用move 而不是copy

以上内容我错了,请忽略。

【讨论】:

  • 这会跳过// init elm.. 位。
  • 在第一个示例的情况下,不会执行省略,因为编译器此时不知道局部变量将超出范围。此外,复制省略与将超出范围的局部变量视为右值完全不同。
  • @Snps 那么我误会编译器可能会将副本优化为移动吗?还是我只是使用了错误的术语?如果是后者,那么这种优化的正确名称是什么?
  • @Cyber​​ 是的,据我所知,您弄错了。不会进行优化。除非使用std::move 明确指定,否则副本永远不会在这种情况下隐式转换为移动。在函数中返回局部变量时,仅在 return 语句中执行对右值的隐式强制转换。
  • 复制省略 (NRVO / RVO) 是对编译器在特定情况下违反 as-if 规则的明确许可。如果那些不存在,它必须遵循 as-if 规则,这可能意味着它不能被消除(至少在我们拥有足够智能的编译器之前)。
【解决方案2】:

尽可能避免动态分配。因此,通常更喜欢将元素本身保存,而不是在向量中保存指向它们的智能指针。

也就是说,两者都可以,如果ThirdPartyElem 是多态的,你就别无选择了。

其他考虑因素是移动和复制类型的成本和可能性,但通常不用担心。

选项一有两个改进可能值得:

  1. std::move 将新元素放到它的位置,因为这可能比复制成本更低(甚至可能不可能)。
    如果类型只能复制而不能移动(旧的,要求更新),则回退到复制。

  2. 尝试就地构建它,以消除复制或移动以及不必要的破坏。

for (int i = 0; i < N; i++)
{
   my_vector.emplace_back();
   try {
       auto&& elm = my_vector.back();
       // init elm..
   } catch(...) {
       my_vector.pop_back();
       throw;
   }
}

如果初始化不能抛出,编译器将删除异常处理(或者你可以省略它)。

【讨论】:

  • 我很好奇,为什么是auto&amp;&amp; 而不是auto&amp;
  • 确实如此。对我来说,这似乎是迄今为止最好的答案。
【解决方案3】:

关注与其他答案略有不同的方面,您正在使用 push_back()。

如果您在进入循环之前知道大小,请考虑这样做

my_vector.resize(N);

这样,你就可以进行数组样式元素的插入了。

my_vector[i] = elem;

你可能会问,有什么好处:

  1. push_back() 每次都会做边界检查,它想插入一个新元素。
  2. 如果您没有执行 reserve(),push_back() 可能偶尔会导致调整大小的损失。
  3. 在数组足够大的情况下,调整大小可能涉及复制大量元素。
  4. 即使您进行了保留 (N) 或构造向量 (N),它仍必须进行边界检查!

当然,如果您正在处理(智能或其他)指针,而不是胖对象,这种方法会更好。在采取这种方法之前,必须权衡建设成本。

在我的测量中,我发现使用 resize() 方法至少可以提高 1.2 倍的性能。

【讨论】:

  • 保留和调整大小不一样; resize 分配并初始化每个元素。如果需要在构造下一个元素之前初始化一个元素,这可能是不可取的,这可能是原始帖子中的情况。另外值得一提的是,push_back 并没有你说的那么低效,因为它保留了一些估计未来需求的能力——它不会在每个 push_back 上重新分配。
  • 同意,值得一提的是,它并不是在每次 push_back 上都调整大小。就我而言,我正在处理智能指针,[]= 明显优于 push_back()。如果您愿意,可以分享数字和代码示例。
【解决方案4】:

存储指针意味着您必须在之后清理它们,或者依靠智能指针为您完成这些,这会增加不必要的间接性和开销。

正如 Cyber​​ 提到的,复制省略可能会阻止复制,但您已经通过使用 std::move 明确避免了这种情况。

既然你提到了 C++11,我建议使用 emplace_back - push_back 和 std::move 应该有相同的结果(参见this question 的答案),但最好是原则上使用 emplace_back;您可以进行的另一项优化,也是最有可能产生重大影响的一项优化,是在一开始就在向量中保留正确的大小,以确保没有不必要的重新分配:

my_vector.reserve(N);
for (int i = 0; i < N; i++)
{
   auto elm = ThirdPartyElm();
   // init elm..
   my_vector.emplace_back(std::move(elm));
}

编辑:根据@Chris Drew 的评论,如果类型不可移动,这不是有效的优化。在这种情况下,如果构造成本高且尽可能避免复制构造,则更稳健的优化是 emplace_back 然后修改新放置的元素:

my_vector.reserve(N);
for (int i = 0; i < N; i++)
{
  my_vector.emplace_back(ThirdPartyElm());       
  my_vector.back().initialise();  // or whatever
}

访问 myvector.back() 有一点额外的开销,但这将比非平凡类型的复制构造成本更低。

【讨论】:

  • 这将仅在ThirdPartyElm 是可移动的情况下进行优化。许多遗留类型不是。
猜你喜欢
  • 1970-01-01
  • 2015-12-31
  • 1970-01-01
  • 2011-08-04
  • 1970-01-01
  • 1970-01-01
  • 2020-10-23
  • 2013-01-04
  • 2013-11-21
相关资源
最近更新 更多