Вопрос или проблема
Я читал о rvalue и наткнулся на что-то, пытаясь запустить фрагмент кода в компиляторе explorer. Вот вымышленный пример:
class A
{
public:
A&& func(A& rhs)
{
//return rhs;
return std::move(rhs); //возвращая A&&
}
};
в сравнении с
class A
{
public:
A func(A& rhs)
{
//return rhs;
return std::move(rhs); //возвращая A&&
}
};
Я ожидал, что этот второй фрагмент не скомпилируется из-за несовпадения типов возвращаемого значения. Возвращаемый тип функции здесь A
, тогда как на самом деле она возвращает A&&
благодаря std::move()
. Я пробовал последние версии gcc, clang и msvc. Все имеют такое же поведение. Может кто-то объяснить, что здесь происходит?
Обновление:
Я думаю, что нашел ответ, ну, по меньшей мере, тот, который имеет смысл для моего маленького мозга. X и X&& — два разных типа, но причина, по которой это работает, в том, что на самом деле есть возможность преобразования через конструктор перемещения по умолчанию. Я изменил фрагмент на это, и компиляция начала вызывать ошибки:
class A
{
public:
A(const A& rhs){};
A(A&& rhs) = delete; //конструктор перемещения удален
A func(A& rhs)
{
//return rhs;
return std::move(rhs);
}
};
<source>(27): error C2280: 'A::A(A &&)': попытка сослаться на удаленную функцию
Это поведение аналогично примеру float one() { return 1; }
, приведенному @JaMiT ниже.
Спасибо всем за ваши комментарии. C++ заставляет меня теряться.
В C++, когда вы используете std::move
на объекте, вы на самом деле не преобразуете его в rvalue-ссылку (T&&
). Вместо этого вы преобразуете его в rvalue, что сигнализирует компилятору о том, что он может рассматривать объект как временный и, если возможно, перемещать его вместо копирования. Однако std::move
не изменяет базовый тип; он просто предоставляет подсказку для компилятора, чтобы использовать семантику перемещения.
Вот что происходит в обоих примерах:
Пример 1: A&& func(A& rhs)
В этой версии функция func
возвращает A&&
(ссылку на rvalue типа A
). Когда вы возвращаете std::move(rhs)
, вы прямо возвращаете ссылку на rvalue, что соответствует возвращаемому типу A&&
. Это имеет смысл и будет компилироваться как ожидалось.
Пример 2: A func(A& rhs)
В этом случае функция func
имеет возвращаемый тип A
, который является значением (не rvalue-ссылкой). Однако, когда вы возвращаете std::move(rhs)
, вы все равно преобразуете rhs
в rvalue-ссылку (A&&
). Вот почему это все еще компилируется и ведет себя так, как вы наблюдали:
-
Автоматическое преобразование: Когда вы указываете тип возвращаемого значения как
A
(значение), компилятор ожидает создать и вернуть объектA
. Если вы предоставляетеA&&
(черезstd::move(rhs)
), компилятор интерпретирует это как инструкцию переместить и сконструироватьA
изrhs
, а не копировать его. Компилятор видит, чтоrhs
является rvalue и использует конструктор перемещения для создания возвращаемого значения. -
Выявление возвращаемого типа и временная материация: Компилятор будет неявно выполнять перемещение при возврате rvalue, если возвращаемый тип является значением. В этом случае
A func(A& rhs)
указывает возвращаемый типA
, поэтому компилятор рассматриваетstd::move(rhs)
как rvalue, создавая временныйA
с использованием конструктора перемещения.
Я ожидал, что этот второй фрагмент не скомпилируется из-за несовпадения типов возвращаемого значения.
Здесь нет проблем с несовпадением.
В вашем втором случае функция возвращает rvalue типа A
.
Это возвращаемое значение будет использоваться с конструктором перемещения класса A
для создания экземпляра A
на стороне вызова.
Это демонстрируется ниже:
#include <iostream>
class A {
public:
A() { std::cout << "default ctor\n"; };
~A() { std::cout << "dtor\n"; };
A([[maybe_unused]] A const & a) { std::cout << "copy ctor\n"; };
A([[maybe_unused]] A && a) { std::cout << "move ctor\n"; };
// Чтобы соблюдать правило 5, нам также нужны копирующий и перемещающий оператор=,
// но здесь это опущено для примера
static A func(A& rhs) {
//return rhs;
return std::move(rhs); //возвращая A&&
}
};
int main() {
A a1;
A a2 = A::func(a1);
}
Вывод:
default ctor
move ctor
dtor
dtor
Если вы не предоставите конструктор перемещения, компилятор сгенерирует его.
Ответ или решение
Вопрос о том, может ли функция в C++ с возвратом по значению возвращать rvalue-ссылку, часто вызывает недоумение, особенно среди начинающих программистов. Давайте подробно разберём эту тему на примере, поднятом в вашем вопросе.
Суть вопроса
Вы привели два примера класса A
, в котором функция func
возвращает разные типы:
- В первом примере функция возвращает rvalue-ссылку типа
A&&
. - Во втором примере функция возвращает объект типа
A
.
В второй версии вы ожидали, что код не скомпилируется, поскольку возвращаемый тип изменяется с rvalue-ссылки на значение.
Объяснение
1. Первое определение: A&& func(A& rhs)
В этом случае функция возвращает rvalue-ссылку. Когда вы используете std::move(rhs)
, вы фактически возвращаете rvalue-ссылку на переданный объект rhs
. Это поведение корректно, и компилятор не выдаёт ошибок, так как возвращаемый тип соответствует типу возвращаемого значения.
2. Второе определение: A func(A& rhs)
В данном определении функция может показаться более сложной. Хотя вы возвращаете std::move(rhs)
, которое имеет тип A&&
, функция объявлена с возвращаемым типом A
. Почему компилятор не выдаёт ошибку?
Это происходит из-за механики C++. Когда функция возвращает значение по типу A
, компилятор автоматически использует перемещение (move semantics), если есть соответствующий конструктор перемещения. Это означает, что объект, возвращаемый функцией, будет временным, и его жизнь будет продлена до конца выражения. Таким образом, происходит следующее:
- Автоматическая конверсия: Компилятор воспринимает вызов
std::move(rhs)
как указание использовать конструктор перемещения для создания возвращаемого объектаA
изrhs
. Так какstd::move
не изменяет тип самого объекта, а только указывает компилятору, что объект может быть перемещён, функция корректно компилируется.
Пример кода
Чтобы проиллюстрировать это, рассмотрим следующий пример кода:
#include <iostream>
class A {
public:
A() { std::cout << "default ctor\n"; }
~A() { std::cout << "dtor\n"; }
A(const A &) { std::cout << "copy ctor\n"; }
A(A &&) { std::cout << "move ctor\n"; }
static A func(A &rhs) {
return std::move(rhs); // возвращаем A и используем std::move
}
};
int main() {
A a1;
A a2 = A::func(a1); // Здесь происходит перемещение
}
Когда вы выполните этот код, вы получите следующий вывод:
default ctor
move ctor
dtor
dtor
Это свидетельствует о том, что перемещение сработало корректно, и объект a2
был создан с использованием конструктора перемещения.
Заключение
Итак, функция, возвращающая значение, действительно может возвращать rvalue-ссылку через перемещение. Надлежащая работа компилятора основана на механизме конструкций перемещения в C++, который предоставляет возможность компилятору использовать перемещение для оптимизации операций с ресурсами. Если бы вы отключили конструктор перемещения класса (сделали его недоступным), вы бы увидели ошибки компиляции, как вы уже сами заметили в вашем обновлении.
Сложность и красота C++ заключаются в его многоуровневой системе типов и механизмах управления ресурсами, и понимание этих нюансов поможет вам стать более опытным разработчиком.