【问题标题】:Example of C++ std::vector<std::auto_ptr<T>> that compiles but fails编译但失败的 C++ std::vector<std::auto_ptr<T>> 示例
【发布时间】:2021-05-21 20:37:59
【问题描述】:

如果std::vector&lt;std::auto_ptr&lt;T&gt;&gt; 编译但无法正确执行,而对于某些数据类型T,具有std::vector&lt;std::unique_ptr&lt;T&gt;&gt; 的同一程序可以正确编译和工作,那将是一个简单的C++ 程序吗?

我知道std::auto_ptr 已被弃用或删除;我只是想要一个涉及容器的示例来激发为什么它被弃用或删除。

我在 MacOS Big Sur 版本 11.2.1 上使用 g++-10 -std=c++20

【问题讨论】:

标签: c++ vector unique-ptr auto-ptr


【解决方案1】:

std::auto_ptr 根本不能在标准容器中使用。在这种情况下,它不能保持适当的语义。这就是移动语义和std::unique_ptr 最初是在 C++11 中发明的原因之一。 std::auto_ptr 在 C++11 中已弃用,并在 C++17 中完全删除。所以不要在现代编码中使用它。

std::auto_ptr 被弃用的官方原因在此处详述:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1856.html#20.4.5%20-%20Class%20template%20auto_ptr

给出的示例在std::vector&lt;std::auto_ptr&lt;int&gt;&gt; 上使用std::sort()

通过这样的设计,我们可以将 auto_ptr 放入一个容器中:

vector<auto_ptr<int> > vec;

但是,这种设计的现场经验揭示了一些微妙的问题。即:

sort(vec.begin(), vec.end(), indirect_less());

根据sort 的实现,上述看起来合理的代码行可能会也可能不会按预期执行,甚至可能崩溃!问题是sort 的某些实现会从序列中挑选一个元素,并存储它的本地副本

...
value_type pivot_element = *mid_point;
...

算法假设在此构造之后pivot_element*mid_point 是等价的。然而,当value_type 变成auto_ptr 时,这个假设失败了,随后算法也失败了。

解决此问题的方法是通过禁止从 const auto_ptr“复制”来使 auto_ptr 无法容纳容器。使用这样的auto_ptr,如果您尝试将其放入容器中,则会出现编译时错误。

最后的结论是这样的:

调用将在auto_ptr 上运行的任何通用代码(无论是否为std)都是有风险的,因为通用代码可能会假定某些看起来像复制操作的东西,实际上副本操作。

结论:

不应该使用复制语法从左值移动。应改为使用其他用于移动的语法。否则,通用代码可能会在想要复制时启动移动。

auto_ptr 使用复制语法从左值移动,因此从根本上来说是不安全的。

【讨论】:

  • 问题是专门要求一个示例,其中使用 std::auto_ptr 编译但无法正确执行。对于 SO 来说,这是否是一个合适的问题还有待观察。 :-)
  • 我的意思是,第一句话直接回答了这个问题。代码示例真的有必要吗?
  • 谢谢 Remy Lebeau,您在 Quicksort 中复制枢轴元素的示例非常清楚地说明了 std::auto_ptr 容器无法正确执行的原因。但是,std::unique_ptr 的容器怎么可能与这个快速排序一起正常工作呢?我的 type_traits 对 auto_ptr 和 unique_ptr 给出了相同的答案(我尝试了 is_copy_constructible、is_move_constructible、is_assignable、is_move_assignable),那么诸如 Quicksort 之类的算法如何区分 auto_ptr 和 unique_ptr 呢?再次感谢。
  • @MarkMeretzky 没必要。引入 unique_ptr 后,sort() 的要求也更新为要求算法支持取消引用迭代器值的移动语义。
【解决方案2】:

auto_ptr的问题在于它可以被复制,并且复制会修改原件:

a = b;   // transfers ownership from b to a

这类似于现在的移动,只是在auto_ptr 的时候语言中没有移动语义。现在 C++ 已经有了 move 语义所有权的转移可以更清楚地表达:

a = std::move(b);

没有人会/应该期望b 在该行中没有被修改。

但是,对于a = b,通常假设b 未被修改。 auto_ptr 打破了这个假设。例如:

template <typename P>
void foo() {
    std::vector<P> x;
    x.resize(42);
    int i=0;
    for (auto& e : x) e.reset(new int(i++));
    
    for (auto e : x) {
        std::cout << *e << "\n";        
    }
    for (auto e : x) {
        std::cout << *e << "\n";        
    }    
}

对于P=std::unique_ptr&lt;int&gt;,这将导致a compiler error

<source>:17:15: error: call to deleted constructor of 'std::unique_ptr<int, std::default_delete<int> >'
    for (auto e : x) {
              ^ ~

虽然它使用 P=std::auto_ptr&lt;int&gt; 编译,但行为未定义(例如此处的段错误:https://godbolt.org/z/93hdse),因为它取消引用空指针。

任何算法的类似问题都假定复制元素是“可以的”。例如,auto_ptr 的比较器按值获取参数会编译但会造成严重破坏:

auto compare = [](auto a,auto b) { return *a < *b; }
std::sort(x.begin(), x.end(),compare);   // BOOM !

复制并不总是那么明显,当元素可复制时,算法可能会在内部复制元素。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-11-17
    • 2018-02-02
    • 2020-07-14
    • 2017-09-01
    • 1970-01-01
    • 2013-10-07
    • 2019-10-20
    相关资源
    最近更新 更多