# Глава 15.2. Нюансы работы со ссылками
Ссылки — это одна из важнейших концепций в C++. И сегодня мы копнем тему ссылок глубже.
## Возврат значения по ссылке
Функция может не только [принимать параметры по ссылке,](/courses/cpp/chapters/cpp_chapter_0151/#block-func) но и возвращать по ссылке результат. Для этого тип возвращаемого значения помечается как ссылочный:
```cpp
T & func_name(params)
{
// ...
}
```
Возвращаемая ссылка может быть константной:
```cpp
const T & func_name(params)
{
// ...
}
```
Главное — помнить, что ссылка должна «смотреть» на живой, ещё не уничтоженный объект. Для этого ссылка должна указывать на:
- Переменную со [статическим временем жизни.](/courses/cpp/chapters/cpp_chapter_0091/#block-static-lifetime) Память под такую переменную выделяется на старте программы, инициализируется переменная при первом обращении (явная инициализация — это тоже обращение), а уничтожается она при завершении программы.
- Ту же переменную, что была передана в функцию по ссылке в качестве аргумента.
- Поле класса. В этом случае ссылку на поле возвращает метод класса.
Что выведется в консоль? {.task_text}
```cpp {.example_for_playground}
import std;
class Singleton {
public:
explicit Singleton(std::string msg) : m_msg(msg) {
std::print("c");
}
~Singleton() {
std::print("d");
}
void say() const {
std::print("{}", m_msg);
}
private:
std::string m_msg;
};
const Singleton & get_singleton() {
static Singleton s{"m"};
return s;
}
int main() {
std::print("1");
const Singleton & ref = get_singleton();
ref.say();
std::print("2");
}
```
```consoleoutput {.task_source #cpp_chapter_0152_task_0080}
```
Память под переменную со статическим временем жизни `s` выделится сразу, но инициализируется переменная только при вызове `get_singleton()`. Поэтому вначале запустится `main()`. После этого при вызове `get_singleton()` выполнится конструктор `Singleton`. Затем вызовется метод `say()`. Произойдет выход из `main()`, а после этого — деструктор `Singleton`. {.task_hint}
```cpp {.task_answer}
1cm2d
```
Допустим, у нас есть класс для хранения ключей и значений. Чтобы при обращении по ключу не создавать копию значения, метод `get()` возвращает ссылку на значение:
```cpp {.example_for_playground .example_for_playground_013}
class Storage
{
public:
// ...
std::string & get(int key) // Возвращаем ссылку на значение
{
return m_data[key];
}
// ...
};
```
При вызове `get()` никакого копирования не происходит. А так как `get()` возвращает неконстантную ссылку, оригинальное значение можно изменять:
```cpp {.example_for_playground .example_for_playground_014}
Storage storage;
std::string & val = storage.get(9);
val = "8f95e06";
```
Это выглядит необычно, но вызов метода, возвращающего ссылку, может стоять по левую сторону от оператора `=`:
```cpp {.example_for_playground .example_for_playground_015}
storage.get(9) = "8f95e06";
```
Если вы возвращаете ссылку на объект, который разрушается при выходе из функции, то ссылка становится висячей.
## Висячие ссылки
Ссылка называется висячей (dangling reference), если уничтожается или перемещается объект, на который она указывает. Обращение по такой ссылке — это UB. А в простых случаях, если компилятор понимает, что ссылка указывает на несуществующий объект, код не компилируется:
```cpp {.example_for_playground}
import std;
// Возвращаем ссылку, а не значение
double & pyramid_volume(double base_area, double height)
{
double res = 1.0 / 3.0 * base_area * height;
return res; // res уничтожается, ссылка на res будет висеть
}
int main()
{
double & v = pyramid_volume(14.3, 6.2); // висячая ссылка
std::println("{}", v); // UB
}
```
```
main.cpp:6:12: error: non-const lvalue reference to type 'double' cannot bind to a temporary of type 'double'
6 | return res;
```
Здесь мы возвращаем ссылку на `res`. Но эта переменная разрушается при выходе из `pyramid_volume()`, ведь её [время жизни](/courses/cpp/chapters/cpp_chapter_0091/#block-lifetime) ограничено телом функции. Переменная `res` локальная, и у неё [автоматическое время жизни.](/courses/cpp/chapters/cpp_chapter_0091/#block-automatic-lifetime) Компилятор разрушает такие переменные, когда они покидают свою область видимости. Поэтому после вызова `pyramid_volume()` ссылка `v` указывает на несуществующий объект.
В главе про область видимости мы [разбирали,](/courses/cpp/chapters/cpp_chapter_0091/#block-stack-frame) что такое [стек вызовов,](https://en.wikipedia.org/wiki/Call_stack) база и вершина стека, а также какую информацию содержат его фреймы. Вообразим, что компилятор все же скомпилировал этот пример кода. Нас интересует, как выглядит стек вызовов в три момента времени:
- выполнение тела `pyramid_volume()`,
- сохранение в `v` результата вызова,
- обращение к переменной `v` внутри `std::println()`.
 {.illustration}
Разберем по шагам, когда ссылка становится висячей и в какой момент в коде появляется UB.
1. Выполняется `pyramid_volume()`. Локальная переменная `res` живет внутри фрейма этой функции. Допустим, у нее адрес `SB - 35`: адрес базы стека (stack base) за вычетом 35 байт. Стек растет от больших адресов к меньшим. Поэтому адреса внутри фрейма `main()` больше, чем внутри `pyramid_volume()`.
2. Выполняется `main()`. Функция `pyramid_volume()` вернула ссылку на `res`, и ею инициализирована переменная `v`. Но указатель на вершину стека сместился с завершившейся `pyramid_volume()` на `main()`. Начиная с этого момента адрес `res` уже нельзя назвать корректным. Доступа к фрейму `pyramid_volume()` больше нет, эта область памяти считается свободной.
3. Выполняется `std::println()`. Фрейм этой функции перезаписывает память, ранее принадлежавшую `pyramid_volume()`. Ссылка `v` теперь указыват на адрес, по которому может находиться все что угодно. И при обращении по этому адресу мы получаем UB.
Что выведется в консоль? {.task_text}
Напишите `err`, если этот код не скомпилируется, или `ub`, если в нем есть неопределённое поведение. {.task_text}
```cpp {.example_for_playground}
import std;
int & next()
{
static int x = 0;
return ++x;
}
int main()
{
next();
int & n = next();
std::println("{}", n);
}
```
```consoleoutput {.task_source #cpp_chapter_0152_task_0090}
```
При вызове `next()` инициализируется статическая переменная `x`. Она будет разрушена после выхода из `main()`. Поэтому ссылка на `x` не будет висячей. {.task_hint}
```cpp {.task_answer}
2
```
Еще одна распространенная ошибка, приводящая к появлению висячих ссылок, — это заведение ссылок на элементы контейнеров, которые могут быть перемещены в памяти.
[Вспомните,](/courses/cpp/chapters/cpp_chapter_0072/#block-vector-under-the-hood) как устроен `std::vector`. Его элементы хранятся в единой области памяти, и при добавлении нового элемента этой памяти [может не хватить.](/courses/cpp/chapters/cpp_chapter_0072/#block-invalidation) Тогда выделяется память большего объема, и все элементы переносятся в нее. В этот момент итераторы на элементы [инвалидируются,](/courses/cpp/chapters/cpp_chapter_0062/#block-invalidation) а ссылки становятся висячими.
```cpp
std::vector<int> data = {5, 9, 8};
int & ref_front = data.front(); // 5
int & ref_middle = data[1]; // 9
data.push_back(10);
// Добавляем еще элементы
// Ссылки ref_front и ref_middle становятся висячими
std::println("{} {}", ref_front, ref_middle); // UB
```
Кстати, многие методы контейнеров возвращают ссылки. У вектора это оператор `[]` и методы `at()`, `front()` и `back()`.
## Продление времени жизни временных объектов
Временный объект (temporary object) — это неименованный объект, который создаётся компилятором для хранения некоего значения. Живет он недолго: как правило, до конца инструкции. Cемантически временный объект подразумевает read-only доступ. Попытка обратиться к нему на запись означает, что в логике программы не всё гладко.
В этом примере функция `http_get()` принимает по константной ссылке параметр `url`. Если вместо именованной переменной передать в функцию литерал, то для его хранения будет создан временный объект. И параметр ссылочного типа `url` будет указывать на него. При выходе из функции временный объект разрушится:
```cpp {.example_for_playground .example_for_playground_017}
import std;
std::string http_get(const std::string & url)
{
// ...
}
int main()
{
// Временный объект "github.com" живёт до конца вызова http_get()
std::string body = http_get("github.com");
std::println("{}", body);
}
```
```
<HTML> ... </HTML>
```
Есть способ продлить время жизни временного объекта: присвоить его _константной_ ссылке.
Иными словами, константную ссылку можно инициализировать неименованным объектом. Тогда его время жизни будет совпадать во временем жизни ссылки. Это называется продлением времени жизни (lifetime extension) или [материализацией временного объекта.](https://en.cppreference.com/w/cpp/language/implicit_conversion.html#Temporary_materialization) (temporary materialization).
В этом примере временный объект `"https://cppreference.com/"` будет разрушен не в конце инструкции, а тогда же, когда и ссылка на него, то есть при выходе из `main()`:
```cpp {.example_for_playground .example_for_playground_018}
const auto & url = std::string("https://cppreference.com/");
std::println("{}", url);
```
```
https://cppreference.com/
```
Обратите внимание, что для продления жизни временного объекта нужна именно константная ссылка. Обычная ссылка не сработает:
```cpp {.example_for_playground .example_for_playground_019}
int main()
{
auto & url = std::string("https://cppreference.com/");
std::println("{}", url);
}
```
```
main.cpp:5:12: error: non-const lvalue reference to type 'basic_string<...>' cannot bind to a temporary of type 'basic_string<...>'
5 | auto & url = std::string("https://cppreference.com/");
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```
## Ссылки на ссылки
У вас [не получится](https://timsong-cpp.github.io/cppwp/n4868/dcl.ref#5) завести ссылку на ссылку. Если вы попытаетесь, то произойдёт [склеивание](https://en.cppreference.com/w/cpp/language/reference.html#Reference_collapsing) или схлопывание ссылок (reference collapsing):
```cpp {.example_for_playground}
import std;
using IntRef = int &;
int main()
{
int x = 10;
IntRef r1 = x; // Тип int &
IntRef & r2 = x; // Тип int &
}
```
Таким образом, тип «ссылки на ссылку ... на ссылку типа `T`» сводится компилятором просто к ссылке на `T`.
## Когда нужно и не нужно использовать ссылки
Основных сценариев применения ссылок всего три:
- Передача _по ссылке_ для изменения объекта.
- Создание _константной ссылки_ для продления жизни временного объекта.
- Передача _по константной ссылке_ для предотвращения копирования тяжёлого объекта.
_Тяжелыми_ как правило считаются объекты, размер которых превышает пару машинных слов. [Машинное слово](https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D1%88%D0%B8%D0%BD%D0%BD%D0%BE%D0%B5_%D1%81%D0%BB%D0%BE%D0%B2%D0%BE) — это единица данных, которую процессор обрабатывает как единое целое. Длина машинного слова (то есть количество бит, которое оно занимает) определяется архитектурой процессора. Например, в архитектуре x86-64 машинное слово занимает 64 бита.
Это означает, что передавать по константной ссылке такие типы как `std::uint64_t`, `double` и `bool` бессмысленно. Зато `std::string`, `std::vector` и другие классы могут иметь большой размер, и их [следует](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rf-in) передавать по константной ссылке.
Как эффективнее передавать тип `EndOfLine` в функцию — по значению или по константной ссылке? Введите `val` или `const ref`. {.task_text}
```cpp
enum class EndOfLine
{
CrLf = 0,
Cr = 1,
Lf = 2,
};
```
```consoleoutput {.task_source #cpp_chapter_0152_task_0100}
```
Базовый тип перечисления — это целое число. {.task_hint}
```cpp {.task_answer}
val
```
Как эффективнее передавать тип `Point` в функцию — по значению или по константной ссылке? Введите `val` или `const ref`. {.task_text}
```cpp
struct Point
{
double x = 0.0;
double y = 0.0;
double z = 0.0;
};
```
```consoleoutput {.task_source #cpp_chapter_0152_task_0110}
```
Размер структуры из 3-х `double` точно превышает пару машинных слов. {.task_hint}
```cpp {.task_answer}
const ref
```
В современном C++ плохой практикой [считается](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rf-out) применение ссылок для возврата из функции нескольких значений. Этот приём можно встретить в старом коде.
В этом примере функция `read_text()` читает файл и возвращает его содержимое типа `std::string`. Но она также записывает в `err_code` код ошибки, которая может произойти при работе с файлом:
```cpp
std::string read_text(const std::string & filename, int & err_code)
{
// ...
}
```
Вызов функции выглядит примерно так:
```cpp {.example_for_playground .example_for_playground_016}
int err = kOk;
std::string contents = read_text("/home/Al/robo_spec.txt", err);
if (err != kOk)
{
// handle error, exit
}
std::println("Successfully read {} bytes from file", contents.size());
```
Работать с функциями, которые по смыслу должны вернуть несколько значений, но по факту возвращают одно, а остальные _изменяют_ через параметры — крайне неудобно. Такой код выглядит странно. Поэтому пользуйтесь альтернативами:
- Кидайте исключение, чтобы сигнализировать об ошибке.
- [Возвращайте](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rf-out-multi) структуру из нескольких полей или пару `std::pair`.
- Возвращайте опциональное значение, обёрнутое в тип `std::optional` или `std::expected`. Эти варианты мы обсудим в следующих главах.
## Домашнее задание
Этот курс знакомит вас с концепцией ссылок уже после того, как вы научились решать на C++ довольно сложные задачи. Мы считаем, что лучше вначале набить руку на использовании контейнеров, итераторов и стандартных алгоритмов, а уже потом постигать тонкости предотвращения лишнего копирования. Поэтому в предыдущих главах _полно_ задач и примеров кода, где ссылки были бы как нельзя кстати.
Откройте [главу про последовательные контейнеры.](/courses/cpp/chapters/cpp_chapter_0072/) Пройдитесь по задачам, в которых требовалось написать функцию. Везде, где считаете необходимым, сделайте параметры константными ссылками.
----------
## Резюме
- Время жизни ссылки не должно превышать время жизни объекта, на который она указывает. Иначе вы получите висячую ссылку.
- Обращение по висячей ссылке — это UB.
- Если функция возвращает значение по ссылке, нужно удостовериться, что ссылка не становится висячей.
- Передавайте по константной ссылке объекты, размер которых превышает пару машинных слов. Это позволит избежать лишнего копирования.
- Чтобы продлить жизнь временного объекта, его можно присвоить константной ссылке.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!