Следующий код не скомпилируется:
#include <utility>
class Foo{
public:
Foo(const Foo& foo) = delete;
explicit Foo(Foo&& foo) = default;
Foo() = default;
Foo bar(){
Foo foo;
return foo;
}
};
с сообщением компилятора:
test_rand.cpp:10:16: error: use of deleted function ‘Foo::Foo(const Foo&)’
10 | return foo;
| ^~~
test_rand.cpp:5:5: note: declared here
5 | Foo(const Foo& foo) = delete;
| ^~~
Подумав, что это потому, что конструктор копирования удален, и когда функция возвращает временную переменную, необходимо создать временную переменную, я добавил std::move
, чтобы сделать foo
значение r, чтобы можно было вызвать конструктор перемещения.
#include <utility>
class Foo{
public:
Foo(const Foo& foo) = delete;
explicit Foo(Foo&& foo) = default;
Foo() = default;
Foo bar(){
Foo foo;
return std::move(foo);
}
};
Однако компилятор выдает мне ту же ошибку: «использование удаленной функции Foo::Foo(const Foo&)».
Затем я попытался удалить ключевое слово explicit
для конструктора перемещения, и все заработало, даже без std::move
.
Интересно, каков для этого внутренний механизм? В частности, каковы подробные шаги компилятора по возврату этого значения только с помощью конструктора перемещения и какие неявные преобразования происходят в процессе возврата?
Сохранив ключевое слово explicit
, я также обнаружил, что если изменить возвращаемую строку на return Foo(std::move(foo))
, ошибка исчезнет. Но в чем разница между этим и return std::move(foo)
, учитывая, что оба они являются значениями. И если я хочу сохранить явный конструктор перемещения, есть ли лучший способ сделать это?
Чтобы сделать это явным, вам понадобится return Foo(std::move(foo));
, не так ли?
«если я хочу сохранить явный конструктор перемещения» > зачем вам это делать? Смотрите также
@ThomasWeller Да, после использования это сработало. Однако я не совсем понимаю разницу между этим и std::move(foo)
, поскольку все они являются значениями.
std::move(foo)
будет рассматривать foo как ссылку на lvalue, поскольку у него есть имя, и попытается преобразовать его в rvalue. explicit
предотвращает преобразование.
Спросите себя, почему вам нужен конструктор ходов explicit
. Какие ситуации вы пытаетесь предотвратить? Теперь вы знаете об одном из последствий, которое это будет иметь. Какая выгода?
Когда вы вызываете bar()
, Foo bar = baz.bar()
и он вызывает return Foo(std::move(foo))
, исключение копирования делает его эквивалентным
Foo bar(std::move(foo)); // explicit move constructor.
Это отлично. Когда Foo::bar()
делает return std::move(foo);
, исключение копирования не применяется и делает это
Foo tmp(std::move(foo));
Foo bar(tmp); // copy constructor.
Это не может скомпилировать конструктор удаленной копии.
Сохранив ключевое слово
explicit
, я также обнаружил, что если изменить возвращаемую строку наreturn Foo(std::move(foo))
, ошибка исчезнет. Но в чем разница между этим иreturn std::move(foo)
, учитывая, что оба они являются значениями.
Оба являются значениями, но что делать с этими значениями? В первом случае вы явно создаете из него объект Foo
(затем срабатывает оптимизация возвращаемого значения, избегая необходимости создавать другой объект). Во втором случае вы просто говорите, что делать со значением r (возвращаете его), подразумевая, что возвращаемый объект будет создан из него (без этого импликации вы бы возвращали Foo&&
вместо Foo
).
Итак, то, что вы видите, является следствием маркировки конструктора explicit
. Конструктор explicit
должен вызываться явно.
Вы также понимаете, почему конструктор перемещения explicit
неудобен...
Объект результата вызова функции инициализируется путем инициализации копирования из операнда оператора return
. Это та же самая инициализация, которую вы могли бы выполнить, например. для параметра функции или для инициализации с синтаксисом инициализатора =
.
Если операнд является значением x, например std::move(tmp)
, но в операторе возврата также просто tmp
, то инициализация копирования приведет к вызову конструктора копирования, поскольку инициализация копирования обычно не учитывает явные конструкторы, точно так же, как и в
Foo a;
Foo b = std::move(a);
или
void f(Foo);
Foo a;
f(std::move(a));
Однако если операндом оператора return является prvalue, например Foo(std::move(tmp))
, то инициализация копирования означает, что объект будет инициализирован из инициализатора prvalue. (Так называемое «исключение обязательного копирования».) Инициализация prvalue Foo(std::move(tmp))
является прямой инициализацией. Таким образом, объект результата вызова функции будет инициализирован путем прямой инициализации из списка аргументов (std::move(tmp))
. В этом разница с предыдущей версией, где он инициализировался копированием с std::move(tmp)
.
При прямой инициализации все конструкторы учитываются по списку аргументов, поэтому можно выбрать явный конструктор перемещения. В этом случае std::move(tmp)
также требуется, потому что tmp
автоматически является значением x в операторе возврата только в том случае, если это целый операнд.
Это то же самое поведение, что и, например. в
Foo a;
Foo b = Foo(std::move(a));
или
void f(Foo);
Foo a;
f(Foo(std::move(a)));
«Если операнд является значением x, например std::move(tmp)
, но в операторе возврата также просто tmp
» — AFAIK, это справедливо начиная с C++23: timsong-cpp.github.io/cppwp/n4950/… . До C++23 tmp
не является xvalue, но вместо него существует другой механизм разрешения перегрузки: timsong-cpp.github.io/cppwp/n4868/class#copy.elision-3.
@DanielLangr Да, верно. Я упростил его, потому что подробное объяснение правил, существовавших до C++23, занимает некоторое время.