c++11copy-constructormove-constructorreturn-by-referencereturn-by-value

Return reference to *this without a copy constructor?


I've written a class similar to the following:

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"
}

As written, g++ fails to compile it at the line marked "ERROR", claiming that it's trying to use the (deleted) copy constructor. However, if I replace the last function with this:

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

It compiles without an error. Even after referring to a number of articles, references, and other SO questions, I don't understand: why does the first invoke the copy constructor at all? Isn't the move constructor enough?


Solution

  • ScriptThread is noncopyable (the implicit copy constructor/assignment operators are defined as deleted because you declared move constructor/assignment). In spawn(), your original implementation:

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

    is attempting to construct a ScriptThread from an lvalue reference (execute returns a ScriptThread&). That will call the copy constructor, which is deleted, hence the error.

    However, in your second attempt:

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

    we run into the rule, from [class.copy]:

    When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the object to be copied is designated by an lvalue, or when the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.

    Even though thread is an lvalue, we perform overload resolution on the constructor of ScriptThread as if it were an rvalue. And we do have a valid constructor for this case: your move constructor/assignment.

    That's why the replacement is valid (and uses move construction), but the original failed to compile (because it required copy construction).