Вернуть ссылку на *this без конструктора копирования?
Я написал класс, подобный следующему:
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++ не может скомпилировать его в строке, помеченной как "ОШИБКА", утверждая, что он пытается использовать (удаленный) конструктор копирования. Однако, если я заменю последнюю функцию на это:
ScriptThread ScriptThread::spawn(const Script &script) {
ScriptThread thread = spawn();
thread.execute(script);
return thread;
}
Компилируется без ошибок. Даже после ссылки на ряд статей, ссылок и других вопросов SO я не понимаю: почему первый вообще вызывает конструктор копирования? Разве конструктора перемещения недостаточно?
2 ответа
ScriptThread
не копируемый (неявные операторы конструктора / присваивания копирования определены как удаленные, потому что вы объявили конструктор / присваивание перемещения). В spawn()
Ваша оригинальная реализация:
ScriptThread ScriptThread::spawn(const Script &script) {
return spawn().execute(script);
}
пытается построить ScriptThread
из ссылки на lvalue (execute
возвращает ScriptThread&
). Это вызовет конструктор копирования, который будет удален, следовательно, ошибка.
Однако со второй попытки:
ScriptThread ScriptThread::spawn(const Script &script) {
ScriptThread thread = spawn();
thread.execute(script);
return thread;
}
мы сталкиваемся с правилом из [class.copy]:
Когда критерии для исключения операции копирования / перемещения выполняются, но не для объявления-исключения, и копируемый объект обозначается lvalue, или когда выражение в операторе возврата является (возможно, заключенным в скобки) идентификатором выражение, которое именует объект с автоматической продолжительностью хранения, объявленным в теле или в параметре-объявлении самой внутренней функции или лямбда-выражения, разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен значением rvalue,
Даже если thread
является lvalue, мы выполняем разрешение перегрузки для конструктора ScriptThread
как будто это было значение. И у нас есть действительный конструктор для этого случая: ваш конструктор / назначение перемещения.
Вот почему замена действительна (и использует конструкцию перемещения), но оригинал не удалось скомпилировать (потому что требовалось создание копии).
execute(script)
возвращает lvalue. Вы не можете неявно переместиться из lvalue, поэтому, чтобы использовать конструктор перемещения для возвращаемого объекта, вам нужно будет сказать
return std::move(spawn().execute(script));
Вы не сделали этого, поэтому он пытается использовать конструктор копирования, потому что именно так вы создаете новые объекты из lvalues.
В вашем случае замены у вас есть:
return thread;
Вот thread
также является lvalue, но он собирается выйти из области видимости, как только функция завершится, поэтому концептуально его можно рассматривать как временную или другую переменную, которая исчезнет в конце выражения. Из-за этого в стандарте C++ существует специальное правило, которое гласит, что компилятор обрабатывает такие локальные переменные как значения r, позволяя использовать конструктор перемещения, даже если thread
действительно lvalue.
См. Более полный ответ Барри для ссылок на стандарт, где определено специальное правило, и полных деталей правила.