Программирование C++C++ CoreРазработчик C++ библиотек

Сравните вызовы методов с ref квалификаторами: какой перегруженный метод будет выбран в каждом случае и поч...

Сравните вызовы методов с ref-квалификаторами: какой перегруженный метод будет выбран в каждом случае и почему?

struct Buffer {
    int use() &  { return 1; }
    int use() && { return 2; }
};

int main() {
    Buffer b;
    int first = b.use();
    int second = Buffer{}.use();
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

b.use() выберет перегрузку use() &, потому что blvalue. Buffer{}.use() выберет use() &&, потому что временный объект — rvalue.

Ref-квалификатор после списка параметров ограничивает вызов метода определённой категорией значения объекта, для которого он вызывается.

Исторический контекст

До появления ref-квалификаторов в C++11 один и тот же неконстантный метод обычно можно было вызвать как для именованного объекта, так и для временного. Это иногда позволяло случайно изменять временные объекты или возвращать из них ссылки, которые становились недействительными после завершения выражения.

Подход с & и && дал классу возможность явно разделить операции для сохраняемых объектов и временных значений. Это особенно полезно для контейнеров, потоковых интерфейсов и классов, которые могут безопасно извлекать ресурсы только из временного объекта.

Постановка проблемы

Категория значения вызывающего объекта влияет на безопасность операции. Метод, возвращающий ссылку на внутренние данные, обычно допустим для lvalue, но опасен для временного объекта: после завершения полного выражения временный объект уничтожается.

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

Подробное решение

Вызов нестатического метода концептуально содержит скрытый параметр *this. Для b.use() этот объект является lvalue, поэтому подходит метод с ref-квалификатором &. Для Buffer{}.use() объект является временным rvalue, поэтому подходит метод с квалификатором &&.

#include <utility> struct Buffer { int value = 42; int& data() & { return value; } int data() && { return value; } }; int main() { Buffer b; int& reference = b.data(); // вызвано data() & int copy = Buffer{}.data(); // вызвано data() && int moved = std::move(b).data(); // вызвано data() && }

Перегрузка data() & возвращает ссылку, поскольку исходный объект b продолжает существовать. Перегрузка data() && возвращает значение: временный объект скоро будет уничтожен, поэтому выдавать ссылку наружу было бы небезопасно.

std::move(b) не перемещает объект сам по себе, а лишь преобразует выражение к xvalue. Поэтому для него также выбирается data() &&; фактическое перемещение или изменение состояния выполняет тело метода.

Ref-квалификаторы можно комбинировать с const, например объявить отдельные варианты read() const & и read() &&. Если подходящая перегрузка отсутствует, вызов становится ошибкой компиляции. Это позволяет не только выбирать реализацию, но и запрещать определённые операции для временных объектов.

Ситуация из практики

Класс Packet предоставляет метод payload(), который возвращает ссылку на внутренний буфер. Без ref-квалификаторов такой вызов мог бы быть разрешён для выражения make_packet().payload(), после чего ссылка указывала бы на уничтоженный временный объект.

Возможны два решения. Можно всегда возвращать копию: это безопасно, но увеличивает затраты. Можно удалить rvalue-перегрузку const std::string& payload() && = delete, чтобы ошибку обнаруживали при компиляции; это безопаснее, но менее удобно для сценариев, где данные можно переместить.

Практичный вариант — предоставить для lvalue ссылку, а для rvalue значение или перемещаемый результат. Такой интерфейс сохраняет эффективность для именованных объектов и не выдаёт ссылку на временный объект.

Что кандидаты часто упускают

  1. Чем T&& в параметрах отличается от ref-квалификатора && у метода?

T&& в параметрах описывает тип аргумента функции и участвует в выводе типов и перегрузке параметров. Ref-квалификатор && относится к скрытому объекту *this и ограничивает категорию значения, на котором разрешён вызов метода. Эти конструкции могут встречаться вместе, но регулируют разные объекты.

  1. Можно ли вызвать метод void f() && для именованного объекта, имеющего тип T?

Обычный вызов object.f() нельзя выполнить, потому что именованное выражение object является lvalue. Вызов станет допустимым после явного преобразования, например std::move(object).f(), но это означает, что метод вправе рассматривать объект как источник ресурсов и изменить его состояние.

  1. Почему const T не превращается автоматически в подходящий вызов T&&?

Квалификатор const и категория значения — разные свойства. Объект типа const T может быть lvalue или rvalue, но неконстантный метод f() && требует неконстантный rvalue-объект. Для константных временных объектов нужна отдельная перегрузка вроде f() const &&; иначе вызов будет запрещён.