【问题标题】:When throwing an object, is it copied or moved?投掷物体时,是复制还是移动?
【发布时间】:2020-03-10 22:38:48
【问题描述】:

据我所知,默认情况下会复制抛出的对象。所以当我抛出一个对象时应该调用复制构造函数。我也知道编译器可能会优化和省略复制。我在这里有一个简单的程序,我在其中抛出了一个名为 X 的类。预计如果我使该类不可复制,则无法抛出该对象。但是意想不到的事情发生了。如果我删除复制构造函数,编译器会抱怨这一点。如果我注释复制构造函数的删除并删除移动构造函数,编译器会抱怨删除移动构造函数。 代码:

class X
{
public:
    int code;
    X(int code) : code(code) {}
    //X(X&) = delete;
    //X(X&&) = delete;
};

void func()
{
    X x(4);
    throw x;
}

int main() { }

删除复制构造函数时的错误:

main.cpp: In function ‘void func()’:
main.cpp:13:11: error: use of deleted function ‘X::X(X&)’
     throw x;
           ^

删除移动构造函数时的错误:

main.cpp: In function ‘void func()’:
main.cpp:13:11: error: use of deleted function ‘X::X(X&&)’
     throw x;
           ^

有人可以解释为什么会这样吗?

编辑:如果我删除复制构造函数并为移动构造函数提供实现,则代码可以正常工作。而如果我删除移动构造函数并为复制构造函数提供一个实现,我仍然会得到同样的错误。为什么我不能删除移动构造函数?

代码:

class X
{
public:
    int code;
    X(int code) : code(code) {}
    X(X&) = default;
    X(X&&) = delete;
};

void func()
{
    X x(5);
    throw x;
}

int main() { }

错误:

main.cpp: In function ‘void func()’:
main.cpp:13:11: error: use of deleted function ‘X::X(X&&)’
     throw x;
           ^

【问题讨论】:

  • 当你“删除复制构造函数”时,是否恢复移动构造函数(这样一些构造函数就剩下了)?
  • @StackExchange123 删除其中一个或两个构造函数之间几乎没有区别。当您删除其中任何一个时,它将删除另一个构造函数。当你删除两者时,它也没有任何区别。
  • @Yksisarvinen 仅当我为移动构造函数提供实现时才有效。如果我删除了移动构造函数,即使我提供了复制构造函数的实现,我仍然会得到同样的错误。
  • @aep 谢谢。我将编辑问题。

标签: c++ exception copy-constructor throw move-constructor


【解决方案1】:

异常对象的初始化遵循与其他对象初始化相同的规则:当初始化器为左值时,使用复制构造函数,当初始化器为右值时,使用移动构造器。但是,请参阅 C++17 [class.copy.elision]/3:

...如果 throw-expression (8.17) 的操作数是范围不扩展的非易失性自动对象(函数或 catch 子句参数除外)的名称超出最内层 try-block 的末尾(如果有),首先执行重载决议以选择副本的构造函数,就好像指定了对象一样 通过右值。如果第一个重载决议失败或未执行,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是 cv 限定的),则再次执行重载决议,将对象视为左值。 [ 注意: 无论是否会发生复制省略,都必须执行此两阶段重载解决方案。它确定如果不执行省略则要调用的构造函数,并且即使调用被省略,所选构造函数也必须是可访问的。 — 尾注 ]

在这里,您将对象扔到首先被视为右值的上下文中。如果重载解析失败,它将被视为左值。因此,一般情况下,编译器更倾向于使用移动构造函数。

  1. 当显式删除复制构造函数时,编译器不会生成移动构造函数([class.copy.ctor]/8),因此代码无法编译。

  2. 当显式删除移动构造函数时,重载决策的第一阶段选择移动构造函数。因重载决议选择的函数被删除而发生错误。 (注意:定义为已删除的默认移动构造函数会被重载决议忽略 ([class.copy.ctor]/10)。重载决议不会忽略显式删除的移动构造函数。)

  3. 当复制和移动构造函数都被显式删除时,上一段仍然成立。

【讨论】:

  • 感谢您的回答。我已经更新了这个问题。如果我删除了移动构造函数并实现了复制构造函数,为什么没有选择复制构造函数?
  • @StackExchange123 删除的函数仍参与重载决议。当重载决议选择一个被删除的函数时,程序是非良构的。
  • @eerorika 哪个规则?
  • @Brian [class.copy.elision]/3,特别是If the first overload resolution fails ... overload resolution is performed again, considering the object as an lvalue。并且当对左值执行重载解析时,选择未删除的复制构造函数。
  • @eerorika 如果重载解析选择了一个被删除的函数,它会成功并且程序格式错误。
【解决方案2】:

引用cppreference

即使复制初始化选择了移动构造函数,从左值复制初始化也必须格式正确,并且析构函数必须是可访问的(C++14 起)

还有

如果expression 的类型是类类型,即使发生复制省略,它的复制/移动构造函数和析构函数也必须是可访问的。

显然,对于定义良好的异常类,复制构造函数是必要的,即使它不会被使用。 C++11 标准 didn't mandate that,但在 C++14 中已修复。

我不是 100% 确定如何解释“这可能会调用右值表达式的移动构造函数”——是否还需要移动构造函数。为了向后兼容,它不应该是,但也许有人可以引用标准。

【讨论】:

  • 删除复制构造函数并实现移动构造函数时没有出现错误,这很奇怪。我只在删除移动构造函数和实现复制构造函数时出错。
【解决方案3】:

投掷物体时,是复制还是移动?

抛出的对象将被复制初始化,左值本地对象将被视为右值 - 与返回本地左值相同。

因此,如果该类型具有移动构造函数,那么它将在您的示例中移动。如果类只能复制但不能移动,那么它将被复制。同样,如果 throw 表达式不是(被视为)右值,则它会被复制。

如果类型既不能复制也不能移动,那么它就不能被抛出。在某些情况下,复制或移动可能会被省略,但这对类型是否必须是可复制的或可移动的没有影响。

【讨论】:

  • 感谢您的回答。我已经更新了这个问题。即使我在删除移动构造函数时提供了复制构造函数的实现,我仍然会得到同样的错误。
  • @StackExchange123 似乎是 MSVC 和旧 GCC 的问题,但不是 Clang 或新 GCC 的问题:gcc.godbolt.org/z/8YkSh-
猜你喜欢
  • 1970-01-01
  • 2014-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-21
  • 1970-01-01
  • 2015-01-22
相关资源
最近更新 更多