【问题标题】:Move which throws?移动哪个投掷?
【发布时间】:2014-02-19 11:12:40
【问题描述】:

据我了解,move-constructors 和 move-assign 必须标记为 noexcept,以便编译器在例如在向量内重新分配时使用它们。

但是,在现实世界中是否存在移动分配、移动构造可能实际抛出的任何实际情况?

更新

例如在构造时具有分配资源的类不能是无掷移动。

【问题讨论】:

  • 我想知道同样的移动。但是为什么不扔掉交换呢?
  • @BЈовић:你需要一个不抛出的交换来获得来自copy-and-swap 和类似习语的强大异常保证。
  • 流对象的移动构造函数可以抛出。
  • @jrok 他们为什么要扔?

标签: c++ exception-handling move-semantics


【解决方案1】:

但是,在现实世界中是否存在移动分配、 move-construct(或swap)实际上可能会抛出?

是的。考虑std::list 的实现。 end 迭代器必须指向列表中“最后一个元素之后的位置”。存在std::list 的实现,其中end 指向的是一个动态分配的节点。甚至默认构造函数也分配了这样一个节点,这样当你调用end()时,就有了指向的东西。

在这样的实现中,每个构造函数必须为end()分配一个节点以指向……甚至是移动构造函数。该分配可能会失败,并引发异常。

同样的行为可以扩展到任何基于节点的容器。

这些基于节点的容器也有实现“短字符串”优化:它们将结束节点嵌入到容器类本身中,而不是动态分配。因此默认构造函数(和移动构造函数)不需要分配任何东西。

如果容器的分配器propagate_on_container_move_assignment::value 为假,并且lhs 中的分配器不等于rhs 中的分配器,则移动赋值运算符可以抛出任何container<X>。在这种情况下,禁止移动赋值运算符将内存所有权从 rhs 转移到 lhs。如果您使用std::allocator,则不会发生这种情况,因为std::allocator 的所有实例彼此相等。

更新

以下是propagate_on_container_move_assignment::value 为假时的一致且可移植的示例。它已经针对最新版本的 VS、gcc 和 clang 进行了测试。

#include <cassert>
#include <cstddef>
#include <iostream>
#include <vector>

template <class T>
class allocator
{
    int id_;
public:
    using value_type    = T;

    allocator(int id) noexcept : id_(id) {}
    template <class U> allocator(allocator<U> const& u) noexcept : id_(u.id_) {}

    value_type*
    allocate(std::size_t n)
    {
        return static_cast<value_type*>(::operator new (n*sizeof(value_type)));
    }

    void
    deallocate(value_type* p, std::size_t) noexcept
    {
        ::operator delete(p);
    }

    template <class U, class V>
    friend
    bool
    operator==(allocator<U> const& x, allocator<V> const& y) noexcept
    {
        return x.id_ == y.id_;
    }
};

template <class T, class U>
bool
operator!=(allocator<T> const& x, allocator<U> const& y) noexcept
{
    return !(x == y);
}

template <class T> using vector = std::vector<T, allocator<T>>;

struct A
{
    static bool time_to_throw;

    A() = default;
    A(const A&) {if (time_to_throw) throw 1;}
    A& operator=(const A&) {if (time_to_throw) throw 1; return *this;}
};

bool A::time_to_throw = false;

int
main()
{
    vector<A> v1(5, A{}, allocator<A>{1});
    vector<A> v2(allocator<A>{2});
    v2 = std::move(v1);
    try
    {
        A::time_to_throw = true;
        v1 = std::move(v2);
        assert(false);
    }
    catch (int i)
    {
        std::cout << i << '\n';
    }
}

这个程序输出:

1

这表明vector&lt;T, A&gt; 移动赋值运算符在propagate_on_container_move_assignment::value 为假且两个分配器比较不相等时复制/移动其元素。如果这些复制/移动中的任何一个抛出,则容器移动分配抛出。

【讨论】:

  • 呵呵,想想看,我自己写了一个 Copy-On-Write 类,它也有一个投掷动作,因为它总是必须分配一个对象。
  • 嗯,仍然没有交换让你失望
  • 但是,如果您可以窃取要移动的对象,为什么还需要分配一个新对象呢?除非您想让对象的移动处于有效状态,但您真的需要这样做吗?
  • 我没有发现这些例子很有说服力。对于std::list,也可以移动动态分配的节点。没有必要在移动时进行分配,除非您想将移动的对象保持在某个僵尸(部分可用)状态。另一个是程序员错误(不能分配不兼容的分配器),应该得到 std::terminate()。
  • @ValentinMilea:随意将您的设计带给 Visual Studio 和 gcc 的实现者:howardhinnant.github.io/container_summary
【解决方案2】:

是的,投掷移动构造函数在野外存在。考虑std::pair&lt;T, U&gt;,其中T 不可移动,U 只能复制(假设副本可以抛出)。然后你就有了一个有用的std::pair&lt;T, U&gt; move 构造函数,它可能会抛出。

如果您需要,标准库中有一个std::move_if_noexcept 实用程序(对于实现std::vector::resize 至少有基本的异常保证很有用)。

另见Move constructors and the Strong Exception Guarantee

【讨论】:

    【解决方案3】:

    在具有const 数据成员的类上移动构造函数也可以抛出。检查here 了解更多信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-11-16
      • 1970-01-01
      • 2023-03-16
      • 1970-01-01
      • 2013-08-07
      • 1970-01-01
      • 2015-10-26
      相关资源
      最近更新 更多