Зачем функции-члены перегружают по квалификатору ссылки на объект и какой вызов это позволяет запретить?
Квалификаторы & и && у функции-члена ограничивают категорию значения объекта, на котором её можно вызвать: & предназначен для lvalue-объектов, а && — для rvalue-объектов. Это позволяет различать безопасный доступ к живому объекту и работу с временным объектом, например запрещать извлечение ссылки из временного значения или, наоборот, эффективно перемещать данные из него.
В C++11 появились rvalue-ссылки и связанные с ними ref-qualified функции-члены. До этого функцию-член без квалификатора ссылки обычно можно было вызвать и для именованного объекта, и для временного объекта, поэтому интерфейс не мог выразить различие между этими случаями.
Проблема особенно заметна у типов-контейнеров и объектов-владельцев ресурсов: для lvalue часто нужно вернуть ссылку без копирования, а для временного объекта выгоднее вернуть значение перемещением.
Вызов функции-члена у временного объекта может завершиться уничтожением этого объекта сразу после полного выражения. Если метод возвращает ссылку или представление во внутренние данные, сохранение результата может привести к обращению к уже уничтоженному объекту.
Обратная ситуация тоже важна: если объект временный и его содержимое больше не понадобится, копирование результата из него неэффективно. Один и тот же метод не всегда способен одновременно выразить безопасный доступ для lvalue и оптимальное потребление данных из rvalue.
Квалификатор & делает функцию-член пригодной для вызова на lvalue-объекте. Квалификатор && выбирается для rvalue-объекта, включая временный объект и результат std::move. При наличии обеих перегрузок выбор выполняется по категории значения неявного объекта.
Типичный интерфейс предоставляет const &-перегрузку для чтения существующего объекта и &&-перегрузку для передачи результата по значению с перемещением. Важно, что const & может быть применима и к временному объекту; если присутствует перегрузка &&, она обычно выбирается для неконстантного rvalue как более подходящая.
Для именованного b вызывается const &-перегрузка, поэтому результатом является ссылка на данные объекта. Для временного Buffer{} вызывается &&-перегрузка, и строка перемещается в result; ссылка на уничтожаемый временный объект наружу не возвращается.
Если операция принципиально небезопасна для временного объекта, можно явно удалить rvalue-перегрузку. При этом вызов на временном объекте будет диагностирован компилятором, а не станет источником висячей ссылки. Следует помнить, что std::move лишь меняет категорию выражения: корректность перемещения и состояние объекта после него определяются самой операцией перемещения.
Квалификатор && не означает автоматического продления времени жизни объекта и не запрещает все вызовы на lvalue без исключения: он ограничивает именно неявный объект функции-члена. Также ref-квалификаторы не заменяют проектирование времени жизни возвращаемых ссылок, представлений и итераторов.
В библиотеке был класс документа с методом получения текста. Метод возвращал const std::string& без ref-квалификаторов. Вызов на временном результате фабрики позволял сохранить эту ссылку, после чего она указывала в уничтоженный документ.
Рассматривались два варианта. Всегда возвращать строку по значению было бы безопаснее и проще для времени жизни, но могло бы создавать лишние копирования при чтении большого числа lvalue-объектов. Запретить все вызовы метода на временных объектах было бы безопасно, но лишило бы API эффективного сценария, когда текст можно переместить из временного документа.
Выбрали две перегрузки: const & возвращает ссылку для lvalue, а && возвращает строку по значению и перемещает её содержимое. В результате чтение существующего документа не копирует строку, а обработка временного документа не сохраняет ссылку на уничтожаемые данные.
&&-перегрузку, чтобы исключить висячую ссылку?Нет. Нужно проверить, что rvalue-перегрузка действительно возвращает безопасный результат по значению либо иным образом не выдаёт ссылку на внутреннее состояние временного объекта. Сам факт наличия && только разделяет вызовы; он не делает тело функции безопасным автоматически.
const & может вызываться для временного объекта?Неявный объектный параметр такой функции аналогичен константной ссылке, которая может связываться с временным объектом. Поэтому одна только const &-перегрузка не является запретом вызова на rvalue. Чтобы выбрать специальную семантику для временного объекта, добавляют &&-перегрузку; чтобы запретить такой вызов, соответствующую перегрузку удаляют или меняют интерфейс.
std::move для объекта, у которого есть только &-квалифицированный метод?Выражение std::move(obj) становится rvalue, поэтому &-квалифицированный метод для него недоступен. Компилятор не обязан автоматически вернуть объект к lvalue и выдаст ошибку, если подходящей &&-перегрузки нет. Это полезная защита: API явно сообщает, что операция разрешена только над объектом, которым владелец продолжает пользоваться как lvalue.