3

考虑以下具有移动构造函数和移动赋值运算符的类:

class my_class
{

    protected:

    double *my_data;
    uint64_t my_data_length;
}

my_class(my_class&& other) noexcept : my_data_length{other.my_data_length}, my_data{other.my_data}
{
    // Steal the data
    other.my_data = nullptr;
    other.my_data_length = 0;
}

const my_class& operator=(my_class&& other) noexcept
{
    // Steal the data
    std::swap(my_data_length, other.my_data_length);
    std::swap(my_data, other.my_data);

    return *this;
}

来这里的目的是noexcept什么?我知道这是对编译器的打击,以下函数不应抛出异常,但这如何启用编译器优化?

4

3 回答 3

8

在https://vimeo.com/channels/ndc2014/97337253noexcept中详细解释了移动构造函数和赋值运算符的特殊重要性

基本上,它不会启用传统意义上的“优化”,即允许编译器生成更好的代码。相反,它允许其他类型(例如库中的容器)在检测到移动元素类型永远不会抛出时采用不同的代码路径。这可以启用替代代码路径,如果它们可以抛出则不安全(例如,因为它会阻止容器满足异常安全保证)。

例如,当您push_back(t)对向量执行操作时,如果向量已满 ( size() == capacity()),则它需要分配一个新的内存块并将所有现有元素复制到新内存中。如果复制任何元素引发异常,则库只会销毁它在新存储中创建的所有元素并释放新内存,保持原始向量不变(从而满足强大的异常安全保证)。将现有元素移动到新存储会更快,但如果移动可以抛出,那么任何已经移动的元素都已经被改变并且不可能满足强保证,因此库只会尝试移动它们它知道不能扔,它只能知道它们是否是noexcept

于 2015-08-26T11:30:44.393 回答
1

恕我直言,使用noexcept不会自行启用任何编译器优化。STL 有以下特点:

std::is_nothrow_move_constructible
std::is_nothrow_move_assignable

像 etc 这样的 STL 容器vector使用这些特征来测试类型 T 并使用移动构造函数和赋值而不是复制构造函数和赋值。

为什么 STL 使用这些特征而不是:

std::is_move_constructible
std::is_move_assignable

答:提供强有力的异常保证。

于 2015-08-26T10:43:28.693 回答
0

首先,我要说的是,在移动构造函数或移动赋值中,什么都不应该抛出,而且似乎没有必要这样做。在构造函数/赋值运算符中唯一必须做的事情是处理已经分配的内存和指向它们的指针。通常,您不应该调用任何其他可以抛出的方法,并且您自己在构造函数/操作符中的移动不需要这样做。但另一方面,调试消息的简单输出打破了这条规则。

可以通过一些不同的方式进行优化。由编译器自动执行,也由使用构造函数和赋值运算符的不同代码实现自动执行。看看 STL,有一些代码的特殊化,如果你使用异常,它们是不同的,它们是通过类型特征实现的。

编译器本身可以更好地优化,同时保证任何代码都不会抛出。编译器通过您的代码有保证的调用树,可以更好地内联,计算编译时间或其他任何东西。可以做的最小优化是不存储有关处理抛出条件所需的实际堆栈帧的所有信息,例如堆栈上的释放变量和其他内容。

这里还有一个问题:noexcept,stack unwinding and performance

也许您的问题与此重复?

我在这里找到了一个与此相关的可能有用的问题:Are move constructors required to be noexcept? 这讨论了投入移动操作的需要。

noexcept 这里的目的是什么?

至少节省一些程序空间,这不仅与移动操作有关,而且与所有功能有关。如果您的类与 STL 容器或算法一起使用,则它可以处理不同的问题,如果您的 STL 实现使用这些信息,则可以带来更好的优化。如果所有其他事情都是编译时间常数,那么编译器可能会因为已知的调用树而获得更好的一般优化。

于 2015-08-26T11:10:12.313 回答