32

可能重复:
当向量增长时如何强制执行移动语义?

insert,push_backemplace( _back) 可能导致重新分配 a std::vector。我很困惑地看到以下代码在重新分配容器时复制元素而不是移动它们。

#include <iostream>
#include <vector>

struct foo {
    int value;

    explicit foo(int value) : value(value) {
        std::cout << "foo(" << value << ")\n";
    }

    foo(foo const& other) noexcept : value(other.value) {
        std::cout << "foo(foo(" << value << "))\n";
    }

    foo(foo&& other) noexcept : value(std::move(other.value)) {
        other.value = -1;
        std::cout << "foo(move(foo(" << value << "))\n";
    }

    ~foo() {
        if (value != -1)
            std::cout << "~foo(" << value << ")\n";
    }
};

int main() {
    std::vector<foo> foos;
    foos.emplace_back(1);
    foos.emplace_back(2);
}

在我使用特定编译器(GCC 4.7)的特定机器上,这将打印以下内容:

foo(1)
foo(2)
foo(foo(1))
~foo(1)
~foo(1)
~foo(2)

但是,当删除复制构造函数 ( foo(foo const&) = delete;) 时,会生成以下(预期的)输出:

foo(1)
foo(2)
foo(move(foo(1))
~foo(1)
~foo(2)

这是为什么?移动通常不会比复制更有效率,或者至少不会低很多吗?

值得注意的是,GCC 4.5.1 做了预期的事情——这是 GCC 4.7 中的回归还是一些巧妙的优化,因为编译器认为我的对象复制起来很便宜(但是如何复制?!)?

另请注意,我确保这由重新分配引起的,通过实验将 afoos.reserve(2);放在插入之前;这不会导致执行复制或移动。

4

3 回答 3

13

简短的回答是我认为@BenVoigt 基本上是正确的。

reserve(§23.3.6.3/2)的描述中,它说:

如果非 CopyInsertable 类型的移动构造函数引发异常,则没有任何影响。

[并且resize§23.3.6.3/12 中的描述要求相同。]

这意味着如果 T 是 CopyInsertable,您将获得强大的异常安全性。为了确保这一点,它只能在推断出(通过未指定的方式)移动构造永远不会抛出的情况下使用移动构造。但是,不能保证任何一个throw()noexcept将是必要的或充分的。如果 T 是 CopyInsertable,它可以简单地选择始终使用复制构造。基本上,正在发生的事情是该标准需要类似复制构造的语义;编译器只能在 as-if 规则下使用移动构造,并且可以自由定义何时或是否会执行该选项。

如果 T 不是 CopyInsertable,则重新分配使用移动构造,但异常安全性取决于 T 的移动构造函数是否可以抛出。如果它不抛出,您将获得强大的异常安全性,但如果它抛出,您不会(我认为您可能会获得基本保证,但可能甚至没有,而且绝对不会更多)。

于 2012-04-12T16:55:36.230 回答
8

Tip-of-trunk clang + libc++ 得到:

foo(1)
foo(2)
foo(move(foo(1))
~foo(2)
~foo(1)

如果您noexcept从移动构造函数中删除 ,那么您将获得复制解决方案:

foo(1)
foo(2)
foo(foo(1))
~foo(1)
~foo(2)
~foo(1)
于 2012-04-12T16:59:33.173 回答
6

这不是回归,而是错误修复。该标准指定 std::vector 将只喜欢不抛出的元素移动构造函数。

另请参阅此说明此错误报告

这个问题也很相关。

于 2012-04-12T16:27:39.110 回答