Функция на C++, возвращающая по значению, может ли возвращать rvalue-ссылку?

Вопрос или проблема

Я читал о 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 возвращает разные типы:

  1. В первом примере функция возвращает rvalue-ссылку типа A&&.
  2. Во втором примере функция возвращает объект типа 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++ заключаются в его многоуровневой системе типов и механизмах управления ресурсами, и понимание этих нюансов поможет вам стать более опытным разработчиком.

Оцените материал
Добавить комментарий

Капча загружается...