移动操作抛出了什么异常?

11
据我理解,在重新分配 vector 内存时,移动构造函数和移动赋值函数必须标记为 noexcept 才能被编译器利用。
然而,是否存在真实世界的情况,即移动构造或移动赋值可能会抛出异常?
更新:当类在构造时拥有已分配的资源时,不能使用不抛异常的移动。

我也在想move的问题。但为什么不使用nothrow swap? - BЈовић
@BЈовић:你需要一个非抛出式交换来从copy-and-swap和类似的习语中获得强异常保证。 - Mike Seymour
流对象的移动构造函数可能会抛出异常。 - jrok
@jrok 为什么他们要放弃? - BЈовић
3个回答

11
然而,在现实世界中,是否存在move-assign、move-construct(或swap)可能会抛出异常的情况呢?
是的。考虑一个std::list的实现。end迭代器必须指向列表中的“最后一个元素的下一个位置”。存在这样一种std::list的实现,即end所指向的是动态分配的节点。即使默认构造函数调用时,也会分配这样的节点,以便在调用end()时,有东西可以被指向。
在这样的实现中,每个构造函数都必须为end()分配一个节点...即使是移动构造函数。这种分配可能会失败,从而抛出异常。
这种行为也可以扩展到任何基于节点的容器。
还有这些基于节点的容器的实现可以进行“短字符串”优化:它们将end节点嵌入到容器类本身中,而不是动态分配。因此,默认构造函数(和移动构造函数)不需要分配任何内容。
如果对于容器的分配器propagate_on_container_move_assignment::value为false,并且lhs中的分配器不等于rhs中的分配器,则移动赋值运算符可能会抛出异常,容器<X>可以出现这种情况。在这种情况下,移动赋值运算符被禁止从rhs转移内存所有权到lhs。如果使用std::allocator,则不会发生这种情况,因为所有的std::allocator实例彼此相等。
更新
以下是propagate_on_container_move_assignment::value为false时的一个符合标准且可移植的示例。它已经针对最新版本的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

这意味着当propagate_on_container_move_assignment::value为false且涉及的两个分配器不相等时,vector<T, A>移动赋值运算符会复制/移动其元素。如果其中任何一个复制/移动操作引发了异常,则容器移动赋值也将引发异常。

想起来了,我自己写过一个Copy-On-Write类,它也有一个抛出异常的动作,因为它总是需要分配一个对象。 - Viktor Sehr
哦,仍然没有抛出异常的交换。 - Viktor Sehr
1
但是如果你可以从正在移动的对象中窃取它,为什么还需要分配一个新对象呢?除非你想让对象的移动保持在有效状态,但你真的需要这样做吗? - Emil Eriksson
我并不认为这些例子十分令人信服。对于 std::list,移动动态分配的节点也是可行的。除非您想保留被移动对象的某些“僵尸”(部分可用)状态,否则没有必要在移动时进行分配。另一个错误是程序员错误(无法分配不兼容的分配器),应该调用 std::terminate()。 - Valentin Milea
@ValentinMilea:随意将您的设计带给Visual Studio和gcc的实现者:http://howardhinnant.github.io/container_summary - Howard Hinnant
@ValentinMilea:在散布错误信息之前,你应该先了解一下 propagate_on_container_move_assignment。这是一个方便的链接:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf - Howard Hinnant

9
是的,实际上有可抛出移动构造函数。考虑 std::pair<T, U>,其中 T 可以使用 noexcept 进行移动操作,而 U 仅支持复制(假设复制可能会抛出异常)。那么你可以得到一个有用的 std::pair<T, U> 移动构造函数,但这个构造函数可能会抛出异常。
如果需要,标准库中有一个 std::move_if_noexcept 实用程序(用于实现至少具有基本异常保证的 std::vector::resize)。
另请参见移动构造函数和强异常保证

1

带有const数据成员的类上的移动构造函数也可能会抛出异常。请查看此处以获取更多信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接