【问题标题】:Return reference to *this without a copy constructor?在没有复制构造函数的情况下返回对 *this 的引用?
【发布时间】:2015-07-21 18:07:04
【问题描述】:

我写了一个类似下面的类:

class ScriptThread {
public:
    ScriptThread(): mParent() {}

private:
    ScriptThread(ScriptThread *parent): mParent(parent) {}

public:
    ScriptThread(ScriptThread &&rhs);
    ScriptThread &operator = (ScriptThread &&rhs);
    // copy constructor/assignment deleted implicitly

    ScriptThread &execute(const Script &script);
    ScriptThread spawn();
    ScriptThread spawn(const Script &script);

private:
    ScriptThread *mParent;
};

ScriptThread &ScriptThread::execute(const Script &script) {
    // start executing the given script
    return *this;
}

ScriptThread ScriptThread::spawn() {
    // create a ScriptThread with "this" as its parent
    return ScriptThread(this);
}

ScriptThread ScriptThread::spawn(const Script &script) {
    // convenience method to spawn and execute at the same time
    return spawn().execute(script); // ERROR: "use of deleted function"
}

正如所写,g++ 未能在标记为“ERROR”的行编译它,声称它正在尝试使用(已删除的)复制构造函数。但是,如果我用这个替换最后一个函数:

ScriptThread ScriptThread::spawn(const Script &script) {
    ScriptThread thread = spawn();
    thread.execute(script);
    return thread;
}

它编译没有错误。即使参考了许多文章、参考资料和其他 SO 问题,我也不明白:为什么第一个调用复制构造函数?移动构造函数还不够吗?

【问题讨论】:

  • execute()的返回类型是ScriptThread&。这是一个左值引用,不能转换为ScriptThread&&

标签: c++11 copy-constructor move-constructor return-by-reference return-by-value


【解决方案1】:

execute(script) 返回一个左值。您不能从左值隐式移动,因此要对返回的对象使用移动构造函数,您需要说

return std::move(spawn().execute(script));

您没有这样做,因此它尝试使用复制构造函数,因为这就是您从左值创建新对象的方式。

在您的替换案例中,您有:

return thread;

这里thread也是一个左值,但是函数一结束就马上超出作用域,所以概念上可以认为是一个临时变量或者其他变量将在表达式的末尾消失。因此,C++ 标准中有一条特殊规则,即编译器将此类局部变量视为右值,允许使用移动构造函数即使thread 确实是左值

有关定义特殊规则的标准的引用以及该规则的完整详细信息,请参阅 Barry 的更完整答案。

【讨论】:

  • 我接受 Barry 的回答主要是为了提供额外的细节和对标准的参考,但我将使用 std::move 代替我所拥有的;它更简洁。感谢你们俩的帮助。
【解决方案2】:

ScriptThread 是不可复制的(隐式复制构造函数/赋值运算符被定义为已删除,因为您声明了移动构造函数/赋值)。在spawn(),您的原始实现:

ScriptThread ScriptThread::spawn(const Script &script) {
    return spawn().execute(script);
}

正在尝试从 左值 引用构造ScriptThreadexecute 返回ScriptThread&)。这将调用复制构造函数,该构造函数被删除,因此出现错误。

但是,在您的第二次尝试中:

ScriptThread ScriptThread::spawn(const Script &script) {
    ScriptThread thread = spawn();
    thread.execute(script);
    return thread;
}

我们遇到了规则,来自 [class.copy]:

当满足省略复制/移动操作的条件,但不满足异常声明时,并且 要复制的对象由左值指定,或者当返回语句中的表达式是(可能 带括号的)id-expression,它命名一个在正文中声明的具有自动存储持续时间的对象parameter-declaration-clause 最里面的封闭函数或 lambda-expression,重载决议 首先执行为副本选择构造函数就好像对象是由右值指定的

尽管thread 是一个左值,我们还是在ScriptThread 的构造函数上执行重载决议,就好像它是一个右值一样。对于这种情况,我们确实有一个有效的构造函数:你的移动构造函数/赋值。

这就是为什么替换是有效的(并使用移动构造),但原来的编译失败(因为它需要复制构造)。

【讨论】:

  • 省略并不是这里的关键;它不会影响代码的良好格式。您需要“隐式视为右值作为回报”规则。
  • @T.C.添加。我的意思是,这是关键的一半。您需要省略标准才能达到该规则。
  • 并非如此。 “or when the expression in a return statement...”子句已经处理了这种情况。
  • @T.C.但是spawn().execute(script); 不是 id-expression
  • 这就是它不被视为右值的原因。我同意省略并不真正相关。 execute(script) 返回一个左值,因此无法从中移动。尽管OP感到惊讶,但这种情况应该是完全显而易见的。你不能从左值移动。 return thread; 可以 被移动的事实应该令人惊讶,但是return thread; 有一个特殊情况允许它被移动,因为thread 即将超出范围所以可以被视为一个 xvalue。
猜你喜欢
  • 2018-04-05
  • 1970-01-01
  • 2011-11-03
  • 2017-04-11
  • 1970-01-01
  • 2015-05-25
  • 1970-01-01
  • 2014-02-03
  • 1970-01-01
相关资源
最近更新 更多