# Глава 15. Ссылки Вы уже [знаете,](/courses/cpp/chapters/cpp_chapter_0141/#block-alias) что в C++ для любого типа можно завести псевдоним. То есть определить новое имя и работать с ним вместо исходного типа. Точно так же можно поступить и с переменной: создать альтернативное имя и обращаться к переменной через него. Это имя и будет называться ссылкой. ## Что такое ссылка [Ссылка](https://www.en.cppreference.com/w/cpp/language/reference.html) (reference) — это псевдоним для существующей переменной. Чтобы объявить ссылку, между типом и именем переменной ставится символ амперсанда `&`. Он означает, что перед вами не обычный тип, а ссылочный. В данном примере у переменной `c_ref` тип `char &`: это ссылка на `char`. ```cpp {.example_for_playground .example_for_playground_001} char c = 'A'; char & c_ref = c; // Ссылка std::println("{}", c_ref); ``` ``` A ``` Разработчику удобно думать про ссылки как про псевдонимы переменных. Но как выглядят ссылки с точки зрения компилятора? Это целиком и полностью зависит от реализации. Стандарт даже [не определяет,](https://timsong-cpp.github.io/cppwp/n4868/dcl.ref#4) выделяется ли под ссылку память. Как правило — нет, [не выделяется.](https://isocpp.org/wiki/faq/references#overview-refs) Каждая переменная живёт в своей области памяти: она связывается с конкретным адресом. И компилятор работает со ссылкой как с адресом исходной переменной. ![Ссылки](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-15/illustrations/cpp/references.jpg) {.illustration} ## Работа со ссылками Читать и изменять значение можно как через исходную переменную, так и через ссылку: ```cpp {.example_for_playground .example_for_playground_002} std::size_t count = 1; std::size_t & n = count; std::println("count={} n={}", count, n); ++n; std::println("count={} n={}", count, n); count += 2; std::println("count={} n={}", count, n); ``` ``` count=1 n=1 count=2 n=2 count=4 n=4 ``` Что выведется в консоль? {.task_text} ```cpp {.example_for_playground .example_for_playground_003} unsigned char x = 255; unsigned char & ref1 = x; unsigned char & ref2 = x; ++ref1; ++ref2; std::println("{} {} {}", x, ref1, ref2); ``` ```consoleoutput {.task_source #cpp_chapter_0150_task_0010} ``` Тип `unsigned char` занимает 1 байт. Значит, 255 — это максимальное для него значение, и при его увеличении произойдёт беззнаковое переполнение. {.task_hint} ```cpp {.task_answer} 1 1 1 ``` Расстановка пробелов вокруг символа `&` роли не играет. Все эти объявления считаются допустимыми: ```cpp T& name; T & name; T &name; ``` Тип ссылки должен совпадать с типом переменной, на которую она указывает. При нарушении этого правила ваш код не скомпилируется: ```cpp {.example_for_playground .example_for_playground_004} bool has_access = false; std::string & ref = has_access; ``` ``` main.cpp:7:19: error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char>') cannot bind to a value of unrelated type 'bool' 7 | std::string & ref = has_access; | ^ ~~~~~~~~~~ ``` В момент создания ссылки её необходимо инициализировать. Не инициализированная ссылка приведёт к ошибке компиляции: ```cpp {.example_for_playground} import std; int main() { std::string & s; } ``` ``` main.cpp:5:19: error: declaration of reference variable 's' requires an initializer 5 | std::string & s; | ^ ``` Ссылка не может быть переназначена после инициализации. Оператор `=` для ссылки изменяет значение переменной, на которую она указывает, а не саму ссылку. Иными словами, все своё время жизни ссылка «смотрит» на один и тот же объект. Что выведется в консоль? {.task_text} ```cpp {.example_for_playground .example_for_playground_005} int a = 1; int b = 5; int & ref = a; ref = b; b = 8; std::println("{} {} {}", a, ref, b); ``` ```consoleoutput {.task_source #cpp_chapter_0150_task_0020} ``` В выражении `ref = b` значение `b` присваивается переменной, на которую указывает ссылка `ref`. {.task_hint} ```cpp {.task_answer} 5 5 8 ``` Как вы считаете, можно ли завести ссылку на `void`? `y/n` {.task_text} ```consoleoutput {.task_source #cpp_chapter_0150_task_0030} ``` У типа `void` пустой набор значений, а ссылка должна указывать на полноценный объект. Поэтому стандарт [запрещает](https://timsong-cpp.github.io/cppwp/n4868/dcl.ref#1) ссылки на `void`. {.task_hint} ```cpp {.task_answer} n ``` ## Передача параметров по ссылке {#block-func} По умолчанию функции принимают параметры по значению (by value): при вызове функции в неё вместо исходных переменных попадают _копии._ Убедимся в этом: ```cpp {.example_for_playground} import std; void sort(std::vector<int> v) { std::sort(v.begin(), v.end()); std::println("Inside function: {}", v); } int main() { std::vector data{5, 0, -1, 2}; std::println("Before calling sort(): {}", data); sort(data); std::println("After calling sort(): {}", data); } ``` ``` Before calling sort(): [5, 0, -1, 2] Inside function: [-1, 0, 2, 5] After calling sort(): [5, 0, -1, 2] ``` Копирование — это дорогая операция, особенно для тяжёлых объектов. Представьте, что в этом примере в функцию `sort()` передаётся массив из миллионов элементов. Он целиком будет скопирован! Так вот, предотвращение лишнего копирования — это наиболее частый сценарий использования ссылок. Так заменим же передачу вектора по значению на передачу по ссылке (by reference). Для этого между типом и именем параметра функции добавим амперсанд: ```cpp void sort(std::vector<int> & v); ``` Когда функция принимает аргумент по ссылке, компилятор связывает существующую переменную с новым именем — параметром функции. То есть заводит для неё псевдоним. И функция получает доступ к исходному объекту вместо копии: ```cpp {.example_for_playground} import std; void sort(std::vector<int> & v) // передаём v по ссылке { std::sort(v.begin(), v.end()); std::println("Inside function: {}", v); } int main() { std::vector data{5, 0, -1, 2}; std::println("Before calling sort(): {}", data); sort(data); std::println("After calling sort(): {}", data); } ``` ``` Before calling sort(): [5, 0, -1, 2] Inside function: [-1, 0, 2, 5] After calling sort(): [-1, 0, 2, 5] ``` Реализуйте функцию `rotate_clockwise()`, которая принимает по ссылке квадратную матрицу `m` и ничего не возвращает. Матрица — это вектор векторов с элементами типа `short`. {.task_text} Функция поворачивает исходную матрицу на 90 градусов по часовой стрелке. В подсказке описан простой вариант реализации поворота. {.task_text} Например, матрица `{{1, 2}, {3, 4}}` после вызова функции превратится в `{{3, 1}, {4, 2}}`: {.task_text} ``` // Исходная матрица [ [1, 2] [3, 4] ] // Поворот на 90 градусов по часовой стрелке [ [3, 1] [4, 2] ] ``` ```cpp {.task_source #cpp_chapter_0150_task_0040} // Ваша реализация rotate_clockwise() ``` Сначала [поменяйте местами](https://en.cppreference.com/w/cpp/algorithm/swap.html) все элементы `m[i][j]` и `m[j][i]`. Это транспонирует матрицу, то есть превратит строки в столбцы, а столбцы — в строки. Затем [инвертируйте](https://cppreference.com/w/cpp/algorithm/reverse.html) порядок элементов в каждом ряду. {.task_hint} ```cpp {.task_answer} void rotate_clockwise(std::vector<std::vector<short>> & m) { for (std::size_t i = 0; i < m.size(); ++i) { for (std::size_t j = i + 1; j < m.size(); ++j) { std::swap(m[i][j], m[j][i]); } } for (std::size_t i = 0; i < m.size(); ++i) std::reverse(m[i].begin(), m[i].end()); } ``` В стандартной библиотеке есть функция [std::swap()](https://en.cppreference.com/w/cpp/algorithm/swap.html), которая принимает два параметра и меняет местами их значения. Разумеется, для этого она принимает оба параметра по ссылке. Так выглядит её объявление: ```cpp namespace std { template<class T> void swap(T & a, T & b); // ... } ``` Реализуйте собственную функцию `swap()`. Она принимает два параметра типа `int` и меняет их местами. {.task_text #block-swap} Например, есть две переменные `x = 1` и `y = 5`. После вызова `swap(x, y)` в `x` должно лежать число `5`, а в `y` — число `1`. {.task_text} ```cpp {.task_source #cpp_chapter_0150_task_0050} // Ваша реализация swap() ``` Функция должна принимать два параметра по ссылке. Чтобы поменять их местами, внутри функции вы можете завести дополнительную переменную. {.task_hint} ```cpp {.task_answer} void swap(int & a, int & b) { int tmp = a; a = b; b = tmp; } ``` ## Использование ссылок в цикле range-for Как вы помните, один из вариантов цикла `for` — это [цикл по диапазону](/courses/cpp/chapters/cpp_chapter_0040/#block-range-for) (range-for): ```cpp for (item : range-initializer) { // ... } ``` В таком цикле удобно перебирать элементы контейнеров: ```cpp {.example_for_playground .example_for_playground_006} std::vector<std::string> available_bluetooth_devices = { "TV 4562", "Sonny's headset", "jbl headphones" }; for(std::string device: available_bluetooth_devices) std::println("{}", device); ``` ``` TV 4562 Sonny's headset jbl headphones ``` Здесь на каждой итерации цикла в переменную `device` попадает _копия_ элемента контейнера `available_bluetooth_devices`. Как неэффективно! Чтобы избежать лишнего копирования, нужно всего лишь поменять тип переменной, через которую перебирается контейнер: ```cpp {.example_for_playground .example_for_playground_007} for(std::string & device: available_bluetooth_devices) std::println("{}", device); ``` Теперь `device` — это ссылка на строку. Мы избавились от копирования при итерировании по контейнеру. Кроме того, теперь мы можем по этой ссылке модифицировать сами элементы, а не их копии: ```cpp {.example_for_playground .example_for_playground_008} for(std::string & device: available_bluetooth_devices) device = "*****"; std::println("{}", available_bluetooth_devices); ``` ``` ["*****", "*****", "*****"] ``` Чтобы достичь схожего результата, до сих пор вам приходилось организовывать циклы с итераторами или циклы по индексам. Кстати, при обращении по индексу или по ключу используется [оператор](https://en.cppreference.com/w/cpp/container/vector/operator_at.html) `[]`. Он возвращает _ссылку_ на элемент контейнера. Именно за счёт этого синтаксис квадратных скобок позволяет работать с элементами контейнера, а не их копиями. ```cpp {.example_for_playground .example_for_playground_009} available_bluetooth_devices[1] = "-"; // Присваиваем значение "-" std::println("{}", available_bluetooth_devices); ``` Реализуйте функцию `lower()`, которая принимает вектор строк и переводит все строки в нижний регистр. {.task_text} Например, строка `"ReFeRenCe"` в нижнем регистре будет выглядеть как `"reference"`. {.task_text} Вам помогут: {.task_text} - Функция [std::tolower()](https://en.cppreference.com/w/cpp/string/byte/tolower.html), которая переводит символ типа `unsigned char` в нижний регистр. - Знание того, что `std::string` состоит из символов `char`, а не `unsigned char`. - Явное приведение типов через [static_cast](https://en.cppreference.com/w/cpp/language/static_cast.html). - Алгоритм [std::transform()](https://en.cppreference.com/w/cpp/algorithm/transform) для применения `std::tolower()` к каждому символу строки. - Цикл `range-for` по ссылкам на элементы вектора. ```cpp {.task_source #cpp_chapter_0150_task_0060} void lower(std::vector<std::string> & words) { } ``` Заведите вспомогательную функцию, которая приводит символ `char` к нижнему регистру: `char make_lower(char c)`. Внутри неё вызовите `std::tolower()` от символа, приведённого к `unsigned char`. Результатом вызова `std::tolower()` будет `unsigned char`. Поэтому его нужно привести обратно к `char`. {.task_hint} ```cpp {.task_answer} char make_lower(char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); } void lower(std::vector<std::string> & words) { for (auto & w: words) std::transform(w.cbegin(), w.cend(), w.begin(), make_lower); } ``` Цикл `range-for` также позволяет итерироваться по контейнерам, хранящим пары ключ-значение. Это особенно удобно в связке с конструкцией [structured binding](/courses/cpp/chapters/cpp_chapter_0073/#block-structured-binding): ```cpp {.example_for_playground .example_for_playground_010} std::map<std::string, std::string> headers = { {"Accept", "text/html"}, {"Accept-Charset", "utf-8"}, {"Cache-Control", "no-cache"} }; for (auto [k, v]: headers) std::println("Header: {}. Value: {}", k, v); ``` ``` Header: Accept. Value: text/html Header: Accept-Charset. Value: utf-8 Header: Cache-Control. Value: no-cache ``` Здесь на каждой итерации цикла в переменную `k` сохраняется копия ключа, а в `v` — копия его значения. Заменим копирование на работу со ссылками на ключи и значения: ```cpp {.example_for_playground .example_for_playground_011} for (auto & [k, v]: headers) std::println("Header: {}. Value: {}", k, v); ``` Уже лучше! Но это код можно ещё чуть-чуть усовершенствовать. Ведь в данном случае нам не требуется возможность изменять значение по ссылке: достаточно только доступа на чтение. А значит, вместо обычной ссылки правильнее использовать константную. ## Ссылки и константность Зачастую требуется сделать так, чтобы через ссылку можно было только читать, но не изменять значение. Именно эту задачу решают константные ссылки. Их также называют ссылками на константные объекты. {#block-const} Чтобы сделать ссылку константной, к её типу добавляется квалификатор `const`. Как и при объявлении любой переменной, тип и `const` могут идти в любом порядке: ```cpp const T & ``` ```cpp T const & ``` В этом примере мы создаём константную ссылку, но пытаемся обратиться к ней на запись. Это приводит к ошибке компиляции: ```cpp {.example_for_playground .example_for_playground_012} int val = 16; const int & ref = val; ++val; // ок std::println("{}", ref); // ок: обращение по ссылке на чтение ++ref; // ошибка: обращение на запись ``` ``` main.cpp:12:5: error: cannot assign to variable 'ref' with const-qualified type 'const int &' 12 | ++ref; | ^ ~~~ ``` Константные ссылки активно используются в функциях, которые должны получить доступ к объекту только на чтение. ```cpp bool is_localhost(const IpAddr & ip) { // Только читаем поля объекта ip } ``` Реализуйте функцию `most_common_word()`, которая принимает два параметра: строку `text` и неупорядоченное множество из строк `stop_words`. Оба параметра передаются по константной ссылке. {.task_text} Функция должна вернуть слово, встречающееся в `text` чаще всего и не входящее в `stop_words`. Строка `text` состоит из слов, разделённых одним пробелом. Считаем, что строка не может начинаться и заканчиваться пробелом. Если она пустая, то функция должна вернуть пустую строку. {.task_text} Например, для `text="a bb a bb"` и `stop_words={"a", "c"}` функция должна вернуть строку `"bb"`. {.task_text} ```cpp {.task_source #cpp_chapter_0150_task_0070} // Ваша реализация most_common_word() ``` Для разбиения текста по пробелам вам помогут методы строки [find()](https://en.cppreference.com/w/cpp/string/basic_string/find.html) и [substr()](https://en.cppreference.com/w/cpp/string/basic_string/substr.html). Для построения частотного словаря слов пригодится `std::unordered_map` с ключами - строками и значениями - количеством их вхождений в `text`. Чтобы найти в этом словаре слово с максимальной частотой, поможет алгоритм [std::max_element()](https://en.cppreference.com/w/cpp/algorithm/max_element.html). {.task_hint} ```cpp {.task_answer} using KV = std::pair<std::string, std::size_t>; bool less(const KV & a, const KV & b) { return a.second < b.second; } std::string most_common_word(const std::string & text, const std::unordered_set<std::string> & stop_words) { if (text.empty()) return {}; std::unordered_map<std::string, std::size_t> freq; const char delim = ' '; std::size_t pos = text.find(delim); std::size_t pos_prev = 0; while(pos != std::string::npos) { std::string word = text.substr(pos_prev, pos - pos_prev); if (!stop_words.contains(word)) freq[word] += 1; pos_prev = pos + 1; pos = text.find(delim, pos_prev); } std::string word = text.substr(pos_prev, std::min(pos, text.size()) - pos_prev + 1); if (!stop_words.contains(word)) freq[word] +=1; auto it = std::max_element(freq.begin(), freq.end(), less); return it->first; } ``` ## Возврат значения по ссылке Функция может не только принимать параметры по ссылке, но и возвращать по ссылке результат. Для этого тип возвращаемого значения помечается как ссылочный: ```cpp T & func_name(params) { // ... } ``` Возвращаемая ссылка может быть константной: ```cpp const T & func_name(params) { // ... } ``` Главное — помнить, что ссылка должна указывать на живой, ещё не уничтоженный объект. Иначе вы получите **висячую ссылку,** которая смотрит на несуществующий объект. Обращение по такой ссылке приведёт к 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); } ``` ``` 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_0090/#block-lifetime) ограничено телом функции. Переменная `res` локальная, и у неё [автоматическое время жизни.](/courses/cpp/chapters/cpp_chapter_0090/#block-automatic-lifetime) Компилятор разрушает такие переменные, когда они покидают свою область видимости. Поэтому после вызова `pyramid_volume()` ссылка `v` указывает на несуществующий объект. Как гарантированно вернуть ссылку на живой объект? Для этого ссылка должна указывать на: - Переменную со [статическим временем жизни.](/courses/cpp/chapters/cpp_chapter_0090/#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_0150_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"; ``` Что выведется в консоль? {.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_0150_task_0090} ``` При вызове `next()` инициализируется статическая переменная `x`. Она будет разрушена после выхода из `main()`. Поэтому ссылка на `x` не будет висячей. {.task_hint} ```cpp {.task_answer} 2 ``` ## Когда нужно и не нужно использовать ссылки Основных сценариев применения ссылок всего два: - Передача _по ссылке_ для изменения объекта. - Передача _по константной ссылке_ для предотвращения копирования тяжёлого объекта. _Тяжелыми_ как правило считаются объекты, размер которых превышает пару машинных слов. [Машинное слово](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_0150_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_0150_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`. Эти варианты мы обсудим в следующих главах. ## Нюансы при работе со ссылками ### Ссылки на ссылки У вас [не получится](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`. ### Продление времени жизни временных объектов Временный объект (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/"); | ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` ### Висячие ссылки Ссылка становится висячей (dangling reference), когда уничтожается или перемещается объект, на который она указывает. Обращение по такой ссылке — это UB. И одна из приводящих к этому распространенных ошибок — заведение ссылок на элементы контейнеров, которые могут быть перемещены в памяти. [Вспомните,](/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_0060/#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()`. ## Домашнее задание Этот курс знакомит вас с концепцией ссылок уже после того, как вы научились решать на C++ довольно сложные задачи. Мы считаем, что лучше вначале набить руку на использовании контейнеров, итераторов и стандартных алгоритмов, а уже потом постигать тонкости предотвращения лишнего копирования. Поэтому в предыдущих главах _полно_ задач и примеров кода, где ссылки были бы как нельзя кстати. Откройте [главу про последовательные контейнеры.](/courses/cpp/chapters/cpp_chapter_0072/) Пройдитесь по задачам, в которых требовалось написать функцию. Везде, где считаете необходимым, сделайте параметры константными ссылками. ---------- ## Резюме - Про ссылку (reference) удобно думать как про псевдоним существующей переменной. - При создании ссылку нужно инициализировать. - Ссылку нельзя переназначить. - Константная ссылка — это ссылка, по которой можно только читать значение, но не изменять его. - Если функция принимает параметр _по значению,_ то при её вызове происходит копирование соответствующего аргумента. - Если функция принимает параметр _по ссылке,_ то она работает с исходным объектом. Копирования не происходит. - Время жизни ссылки не должно превышать время жизни объекта, на который она указывает. Иначе вы получите висячую ссылку, обращение по которой приведёт к UB. - Передавайте по константной ссылке объекты, размер которых превышает пару машинных слов. Это позволит избежать лишнего копирования.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!