Возврат по значению или по ссылке rvalue из квалифицированной функции-члена ссылки rvalue?

В статье 12 Effective Modern C++ Скотт Мейерс пишет: следующий класс, чтобы показать, насколько полезной может быть перегрузка функций-членов для ссылочных квалификаторов:

class Widget {
public:
    using DataType = std::vector<double>;
    …
    DataType& data() &            // for lvalue Widgets
    { return values; }            // return lvalue

    DataType data() &&            // for rvalue Widgets
    { return std::move(values); } // return rvalue
    …
private:
    DataType values;
};

Это кажется очевидным: теперь non_temp_obj.data() вызовет первую перегрузку и вернет ссылку на член объекта, который после этого все еще жив, тогда как make_temp_obj().data() возвращает по значению член объекта, который умирает, как только это выражение выполняется.

Вот мой первый вопрос: что касается перегрузки &&, почему return std::move(values);, а не только return values;, учитывая, что мы возвращаем значение?

Однако в об ошибках Мейерс пишет:

Лучший способ заставить перегруженную ссылку rvalue функции-члена data возвращать rvalue — вернуть ссылку rvalue. Это позволило бы избежать создания временного объекта для возвращаемого значения и соответствовало бы возврату по ссылке исходного интерфейса data вверху страницы 84.

что я интерпретирую как предложение изменить

    DataType data() &&
    { return std::move(values); }

to

    DataType&& data() &&
    { return std::move(values); }

но я не понимаю причины, особенно в свете этого ответа, который в значительной степени убеждает меня в том, что версия книги верна и опечатка неправильная.

Итак, мой второй вопрос: кто прав?


person Enlico    schedule 03.11.2020    source источник


Ответы (1)


values является членом объекта и lvalue, поэтому, если вы просто return values напрямую, он будет скопирован в возвращаемое значение, а не перемещен. Смысл перегрузки && ref-qualified состоит в том, чтобы избежать создания ненужной копии. return std::move(values) выполняет это путем приведения values к значению r, так что оно перемещается, а не копируется.

Что касается второй части вашего вопроса: у обоих есть свои преимущества и недостатки. Как указано в ответе, который вы связали, возврат по значению из перегрузки && позволяет избежать проблем со временем жизни, поскольку время жизни возвращаемого объекта будет продлено, если ссылка будет немедленно привязана к нему. С другой стороны, возврат по значению может неожиданно уничтожить значение values. Например:

DataType Widget::data() &&
{ return std::move(values); }

void func() {
    Widget foo;
    std::move(foo).data(); // Moves-constructs a temporary from
                           // foo::value, which is then immediately
                           // destroyed.
    auto bar = foo.data(); // Oops, foo::value was already moved from
                           // above and its value is likely gone.
}
person Miles Budnek    schedule 03.11.2020
comment
Во-первых, не должен ли вмешаться RVO и избегать копирования, а также перемещения? - person Enlico; 04.11.2020
comment
Нет, RVO применяется только к локальным переменным функции, а не к членам объекта. Точно так же локальные переменные функции автоматически перемещаются при возврате, даже если RVO не применяется, но опять же это не применяется к членам объекта. - person Miles Budnek; 04.11.2020
comment
Не знаю, почему это неожиданно. std::move(foo) явно говорит, что объект может быть перемещен, поэтому я ожидаю, что состояние изменится после этого выражения, иначе я бы не std::move. - person Jens; 04.11.2020
comment
ИМО, основными минусами возврата по значению является дополнительный конструктор перемещения (который не обязательно дешев, как для std::array). - person Jarod42; 04.11.2020
comment
@MilesBudnek, как возвращение DataType&& в вашем примере изменит то, что происходит? Может быть, std::move(foo).data(); будет создавать и уничтожать ссылку на foo::value, тем самым оставляя последнюю нетронутой, когда вы определяете bar? - person Enlico; 06.11.2020
comment
@ Энрико Верно. Возврат ссылки будет означать, что новый объект не создается как часть этого вызова, и поэтому нет ничего, что могло бы украсть какие-либо ресурсы, принадлежащие foo.value. - person Miles Budnek; 06.11.2020