Возврат объекта только с явным конструктором перемещения

Следующий код не скомпилируется:

#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), учитывая, что оба они являются значениями. И если я хочу сохранить явный конструктор перемещения, есть ли лучший способ сделать это?

Copy elision может выполнить перемещение за вас или даже избавиться от копии. Какую версию C++ вы используете?
Thomas Weller 04.07.2024 21:31

Чтобы сделать это явным, вам понадобится return Foo(std::move(foo));, не так ли?

Thomas Weller 04.07.2024 21:32

«если я хочу сохранить явный конструктор перемещения» > зачем вам это делать? Смотрите также

Nelfeal 04.07.2024 21:37

@ThomasWeller Да, после использования это сработало. Однако я не совсем понимаю разницу между этим и std::move(foo), поскольку все они являются значениями.

ttzytt 04.07.2024 21:38
std::move(foo) будет рассматривать foo как ссылку на lvalue, поскольку у него есть имя, и попытается преобразовать его в rvalue. explicit предотвращает преобразование.
doug 04.07.2024 21:51

Спросите себя, почему вам нужен конструктор ходов explicit. Какие ситуации вы пытаетесь предотвратить? Теперь вы знаете об одном из последствий, которое это будет иметь. Какая выгода?

Ted Lyngmo 04.07.2024 22:00
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
113
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Когда вы вызываете 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.

Daniel Langr 05.07.2024 11:04

@DanielLangr Да, верно. Я упростил его, потому что подробное объяснение правил, существовавших до C++23, занимает некоторое время.

user17732522 05.07.2024 18:04

Другие вопросы по теме