Зачем нужны ссылки в c
Перейти к содержимому

Зачем нужны ссылки в c

  • автор:

Ссылки в C++

Ссылка в С++ — это альтернативное имя объекта.

Ссылку можно понимать как безопасный вариант указателя. При этом ссылки имеют особенности, отличающие их от указателей:

  1. При объявлении ссылка обязательно на уже существующий объект данного типа. Ссылка не может ссылаться «ни на что».
  2. Ссылка от её объявления до её исчезновения указывает на один и тот же адрес.
  3. При обращении к ссылке разыменование происходит автоматически.
  4. Адрес ссылки — это адрес исходного объекта, на который она указывает.

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

int &x; // недопустимо!
int &x = veryLongVariableName; // допустимо. Теперь x — это альтернативное имя переменной veryLongVariableName

int A[10];
int &x = A[5]; // Ссылка может указывать на элемент массива
x++; // то же, что A[5]++;
x = 1; // то же, что A[5] = 1;

Передача параметров в функцию по ссылке

Параметры можно передавать по ссылкам. При этом связывание ссылки с определённой переменной произойдёт в момент вызова функции (на этапе выполнения программы).

void foo(int &x)
x = 3;
>
int main()
int t = 1;
foo (t);
cout >

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

void foo(const int &x);

Это гарантирует программисту-пользователю функции неизменность передаваемого значения.

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

Чего нельзя делать со ссылкой

Возврат из функции ссылки на автоматически созданный объект (локальную переменную) приводит к появлению «битых ссылок», значение которых непредсказуемо.

Также синтаксис С++ не позволяет создавать указатели на ссылки и массивы ссылок.

Зачем нужны ссылки на указатели?

Отсюда вопрос, зачем в С++ вообще существует подобный синтаксис, ведь strange_ref, несмотря на то, что является типом int*&, все равно будет иметь такой же функционал, как и int*. Все также можно будет разыменовать эту ссылку и тд. Почему этот синтаксис вообще существует и где он применяется?

Отслеживать
123k 24 24 золотых знака 127 127 серебряных знаков 304 304 бронзовых знака
задан 25 сен 2021 в 18:57
ComeInRage ComeInRage
1,601 6 6 серебряных знаков 14 14 бронзовых знаков
void change_ptr(int *&p) < p = . ; >
25 сен 2021 в 19:03
Неявно такой синтаксис возникает в шаблонах.
25 сен 2021 в 19:04
@StanislavVolodarskiy А можно немного подробнее? Не понятно из этого короткого примера
25 сен 2021 в 19:24
В ответе ниже сразу обе ситуации: меняем указатели местами в шаблонной функции.
25 сен 2021 в 20:55

4 ответа 4

Сортировка: Сброс на вариант по умолчанию

Ну вот конкретный практический пример: типичная функция навроде swap без проблем работает с указателями, принимая их по ссылке, как и другие объекты:

template void swap(T & left, T & right) < T tmp; left = right; right = tmp; > 

Или несколько операций с предварительно выбранным одним указателем:

int * p1<>; int * p2<>; int * & pcur; . // много операций, изменяющих pcur; 

Собственно указатели являются такими же объектами, как и все остальные, и для них можно использовать все те же сценарии работы со ссылками.

Отслеживать
ответ дан 25 сен 2021 в 20:02
user7860670 user7860670
29.8k 3 3 золотых знака 17 17 серебряных знаков 36 36 бронзовых знаков

Но какой смысл принимать указатель по ссылке, в чем профит? Или это сделано лишь для того, чтобы можно было принимать различные типы, помимо указателей, например в шаблонах?

25 сен 2021 в 20:39

@ComeInRage Если принимать указатель не по ссылке, а по значению, то тогда бы исходный указатель не изменился. А если бы по указателю, то получился бы нужен еще один слой из разыменований.

25 сен 2021 в 20:45

несмотря на то, что является типом int*&, все равно будет иметь такой же функционал, как и int*

int x = 1, y = 2; int *a = &x; int *b = a; int *&c = a; a = &y; std::cout  

Отслеживать
123k 24 24 золотых знака 127 127 серебряных знаков 304 304 бронзовых знака
ответ дан 25 сен 2021 в 19:04
HolyBlackCat HolyBlackCat
27.2k 3 3 золотых знака 27 27 серебряных знаков 40 40 бронзовых знаков

Спасибо за ответ, но у вас указателю присваивается int. Из-за этого не совсем понятно, что вы хотели сказать)

25 сен 2021 в 19:18

Однако я понял в чем разница. Но все равно не понятно, зачем этот синтаксис вообще нужен. Но спасибо за ответ, хоть прояснилась ситуация

25 сен 2021 в 19:24

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

25 сен 2021 в 19:40
@ComeInRage, ну пропустил он один амперсанд.
25 сен 2021 в 20:17

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

 size_t f(int *p) < size_t s; s=(512+((rand())% 1024)); p=new int [s]; return s; >int main ()

Здесь p – указатель не на x, а на память, выделенную оператором new. А где лежит x? Функции надо знать именно это, только тогда она сможет положить в этот x указатель на память, выделенную оператором new. Куда класть адрес выделенной памяти? В x? Или в y?

 size_t f(int *&p) < size_t s; s=(512+((rand())% 1024)); p=new int [s]; return s; >int main ()

А вот здесь всё в порядке, p – ссылка на x, теперь можно в этот x положить значение, возвращённое оператором new. А вот x уже будет указателем на память, выделенную по new. Или Вы не это имели ввиду?

Отслеживать
ответ дан 9 ноя 2022 в 6:26
Тарас Атавин Тарас Атавин
204 1 1 серебряный знак 9 9 бронзовых знаков

Могу дополнить уже данные ответы своей интерпретацией. Возможно кому-то так будет проще разобраться.

Смысл ссылки на указатель в том чтобы работать с исходным указателем. дело в том, что если передать просто указатель - это работает как передача по значению, то есть изменения переданного указателя внутри метода или функции не приводят к изменению значения исходного (переданного) указателя. изменится только его локальная копия. если вам нужно модифицировать не объект, расположенный в памяти на которую указывает аргумент, а сам адрес на который указывает аргумент - кажется нет другого выхода кроме как передавать указатель по ссылке (ссылку на указатель). да, синтаксис при этом останется как при использовании обычного указателя.

Ссылки

Ссылка reference — механизм языка программирования (C++), позволяющий привязать имя к значению. В частности, ссылка позволяет дать дополнительное имя переменной и передавать в функции сами переменные, а не значения переменных.

Синтаксически ссылка оформляется добавлением знака & (амперсанд) после имени типа. Ссылка на ссылку невозможна.

Ссылка требует инициализации. В момент инициализации происходит привязка ссылки к тому, что указано справа от = . После инициализации ссылку нельзя “отвязать” или “перепривязать”.

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

int n = 0; int &r = n; /* теперь r -- ссылка на n или второе имя переменной n */ n = 10; cout '\n'; // выведет 10 r = 20; cout '\n'; // выведет 20 cout '\n'; // выведет 1, т.е. истина

Казалось бы, зачем нам второе имя переменной? Ответа может быть, по крайней мере, два.

  1. Что-то имеет слишком длинное, неудобное название. Привязав к нему ссылку, мы получим более удобное, короткое локальное название. При этом мы можем не указывать тип этого “чего-то”, можно использовать вместо типа ключевое слово auto :
auto &short_name = some_namespace::some_long_long_name;
  1. Выбор объекта привязки ссылки может происходить во время исполнения программы и зависеть от некоего условия. Пример:
int a = 0, b = 0; cin >> a >> b; int &max = a < b? b: a; // привязать к b, если a < b, иначе -- к amax = 42; cout "a = " << a "; b = " << b '\n';

Впрочем, основным применением ссылок является передача параметров в функции “по ссылке” и возвращение функциями ссылок на некие внешние объекты.

Передача по ссылке by reference напоминает передачу “по имени”. Таким образом, можно сказать, что, используя ссылки, мы передаём не значения, а сами переменные, содержащие эти значения. В реальности “за ширмой” происходит передача адресов этих переменных. Передача ссылки на переменную, время жизни которой заканчивается, например, возврат из функции ссылки на локальную переменную, приводит к неопределённому поведению.

Ранний пример использования ссылок для возврата из функции более одного значения представлен в самостоятельной работе 3.

Приведём здесь ещё один пример: функцию, которая возвращает одну из двух переменных, содержащую максимальное значение. Для этого модифицируем предыдущий пример:

int& max_byref(int &a, int &b) < return a < b? b: a; >int main() < int x = 0, y = 0; // собственно имена переменных не обязаны совпадать cin >> x >> y; max_byref(x, y) = 42; cout "x = " << x "; y = " << y '\n'; return 0; >

Так как при передаче ссылки реально копируется лишь адрес значения, а не само значение, то передав ссылку можно избежать копирования значения. Поэтому ссылки широко используются для передачи в функцию аргументов, которые или запрещено копировать или вычислительно дорого копировать. Типичный пример — объекты string. При копировании строки происходит выделение динамической памяти, копирование всех символов, затем — при удалении этой копии — освобождение памяти. Часто нет никакой необходимости в копировании. Например, следующей функции, считающей количество повторений заданного символа в строке нет нужды копировать строку — можно обойтись ссылкой:

size_t char_freq(const string &s, char c) < size_t freq = 0; for (size_t i = 0, sz = s.size(); i != sz; ++i) freq += s[i] == c; return freq; >

Обратите внимание на ключевое слово const . Данное ключевое слово позволяет нам указать, что мы хотим ссылку на константу, т.е. функция char_freq использует s как константу и не пытается её изменять, а ссылка нужна для того, чтобы избежать копирования. Рекомендуется использовать const везде, где достаточно константы. Компилятор проверит, действительно ли мы соблюдаем константность.

Ставить слово const можно перед именем типа и после имени типа, это эквивалентные записи.

int x; const int &r1 = x; // ссылка на x "только для чтения" int const &r2 = x; // тоже ссылка на x "только для чтения" int & const r3 = x; // ошибка компиляции, нельзя ставить const после &

Указатели

Общие сведения

Что такое указатель pointer уже рассказывалось во введении.

В C и C++ указатель определяется с помощью символа * после типа данных, на которые этот указатель будет указывать.

Указатель — старший родственник ссылки. Указатели активно использовались ещё в машинных языках и оттуда были перенесены в C. Ссылки же доступны только в C++.

Указатели — простые переменные. Указатели не “делают вид”, что они — те значения в памяти, к которым они привязаны. Чтобы получить указатель на переменную, нужно явно взять её адрес с помощью оператора & . Чтобы обратиться к переменной, на которую указывает указатель, требуется явно разыменовать его с помощью оператора * .

int n = 0; int *r = &n; // теперь r -- указатель на n n = 10; cout '\n'; // выведет 10 *r = 20; cout '\n'; // выведет 20 cout '\n'; // выведет 1

Так же, как и в случае ссылок, можно использовать ключевое слово const , чтобы создать указатель на константу.

int x = 0, y = 1; const int *p1 = &x; // указатель на x "только для чтения" y = *p1; // можно *p1 = 10; // ошибка компиляции: нельзя изменить константу *p1 p1 = &y; // можно: сам указатель p1 не является константой int const *p2 = &x; // тоже указатель на x "только для чтения", всё аналогично p1 int * const p3 = &x; // теперь константа -- сам указатель y = *p3; // можно *p3 = 10; // тоже можно! p3 = &y; // ошибка компиляции: нельзя изменить константу p3 const int * const p4 = &x; /* комбо: теперь у нас константный указатель на x "только для чтения" */ y = *p4; // можно *p4 = 10; // ошибка компиляции: нельзя изменить константу *p4 p4 = &y; // ошибка компиляции: нельзя изменить константу p4

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

Указатели можно передавать в функции и возвращать из функций как и любые “элементарные” значения. Ещё пример с указателями:

int* max_byptr(int *a, int *b) < return *a < *b? b: a; >int main() < int x = 0, y = 0; // собственно имена переменных не обязаны совпадать cin >> x >> y; *max_byref(&x, &y) = 42; cout "x = " << x "; y = " << y '\n'; return 0; >

Для обращения к полю структуры по указателю на объект структуры предусмотрен специальный оператор -> (“стрелка”).

struct Point < float x, y; >; Point a = < 20, 30 >; cout ' ' '\n'; // > 20 30 Point *p = &a; p->x = 42; (*p).y = 23; // то же самое, что p->y = 23; cout ' ' '\n'; // > 42 23

В отличие от ссылок, указатели не обязательно инициализировать. Указатели можно инициализировать специальным значением нулевой указатель nullptr , которое сигнализирует об отсутствии привязки указателя к чему-либо. Присваивание указателю другого адреса меняет его привязку. Это позволяет использовать указатели там, где семантика ссылок слишком сильно ограничивает наши возможности.

Соответственно, ограничения, накладываемые на ссылки по сравнению с указателями, позволяют, с одной стороны, защитить программиста от ряда ошибок, и, с другой стороны, открывают ряд возможностей оптимизации кода для компилятора. Ссылки используются там, где нет нужды в “полноценных” указателях или есть желание не перегружать код взятиями адреса и разыменованиями.

Наличие нулевого указателя позволяет, например, возвращать указатель на искомый объект и в том случае, когда ничего не было найдено. Просто в этой ситуации возвращаем нулевой указатель, а принимающая сторона должна быть готова к такому развитию событий. Указатель автоматически преобразуется к булевскому значению: нулевой указатель даёт false , прочие указатели дают true , поэтому, если p — указатель, то

if (p) . 

есть то же самое, что

if (p != nullptr) . 
if (!p) . 

есть то же самое, что

if (p == nullptr) . 

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

// Ищет нулевой элемент в диапазоне [from, to). // Возвращает нулевой указатель, если нуль не был найден. float* find_next_zero(float *from, float *to) < for (; from != to; ++from) if (*from == 0.f) return from; // нашли return nullptr; // ничего не нашли > int main() < float num[] < 1, 2, 3, 0, 3, 4 >; if (auto zero_pos = find_next_zero(num, num + sizeof(num)/sizeof(num[0]))) cout '\n'; else cout "zero not found\n"; // невозможно! return 0; >

Данный пример использует арифметику указателей и массивы. Данная тема освещена в разделе массивы и ссылки.

Бестиповый указатель

Вместо типа данных при объявлении указателя можно поставить ключевое слово void . Данное ключевое слово означает, что мы описываем указатель “на что угодно”, т. е. просто адрес в памяти. Любой указатель автоматически приводится к типу void* — бестиповому указателю typeless pointer . Прочие указатели, соответственно, называются типизированными или типизованными typed . Приведение от void* к типизованному указателю возможно с помощью оператора явного приведения типа.

В C бестиповые указатели широко применяются для оперирования кусками памяти или реализации обобщённых функций, которые могут работать со значениями разных типов. В последнем случае конкретный тип маскируется с помощью void (“пустышка”). При использовании таких функций обычно приходится где-то явно приводить тип указателей. C++ позволяет отказаться от подобной практики благодаря поддержке полиморфизма и обобщённого программирования (материал 2-го семестра).

#include #include // setw -- ширина поля вывода, hex -- вывод в 16-ричной системе #include using namespace std; // Ещё один способ получить битовое представление числа с плавающей точкой. int main() < unsigned char buffer[sizeof(float)]; // Настройка потока вывода. cout.fill('0'); // Заполнять нулями. cout.setf(ios::right); // Выравнивать по правому краю. for (float x; cin >> x; ) < // Скопировать побайтово память x в память buffer. memcpy(buffer, &x, sizeof(float)); // Вывести каждый байт buffer в 16-ричной форме. for (int byte: buffer) cout 2) ' '; cout '\n'; > >

О цикле for (int byte: buffer) см. здесь.

Указатель на указатель

Так как указатель — обычная переменная, возможен указатель на указатель. И указатель на указатель на указатель. И указатель (на указатель) n раз для натурального n. Максимальный уровень вложенности задаётся компилятором, но на практике уровни больше 2 практически не используются.

int n = 4; int *p = &n; // уровень косвенности 1 *p = 5; cout // выведет 5 int **pp = &p; // уровень косвенности 2 **p = 6; cout // выведет 6 int ***ppp = &pp; // уровень косвенности 3 ***p = 7; cout // выведет 7

Система ранжирования C-программистов.

Чем выше уровень косвенности ваших указателей (т. е. чем больше “*” перед вашими переменными), тем выше ваша репутация. Беззвёздочных C-программистов практически не бывает, так как практически все нетривиальные программы требуют использования указателей. Большинство являются однозвёздочными программистами. В старые времена (ну хорошо, я молод, поэтому это старые времена на мой взгляд) тот, кто случайно сталкивался с кодом, созданный трёхзвёздочным программистом, приходил в благоговейный трепет.

Некоторые даже утверждали, что видели трёхзвёздочный код, в котором указатели на функции применялись более чем на одном уровне косвенности. Как по мне, так эти рассказы столь же правдивы, сколь рассказы об НЛО.

Просто чтобы было ясно: если вас назвали Трёхзвёздочным Программистом, то обычно это не комплимент."

Условия для проверки себя на “трёхзвёздность” перечислены на другой странице того же сайта.

В случае C указатели на указатели (уровень косвенности 2) используются довольно часто, например, для возвращения указателя из функции, которая возвращает ещё что-то, или для организации двумерных массивов. Пример такой функции из Windows API:

DWORD WINAPI GetFullPathName( _In_ LPCTSTR lpFileName, _In_ DWORD nBufferLength, _Out_ LPTSTR lpBuffer, _Out_ LPTSTR *lpFilePart );

Функция принимает имя файла как указатель на си-строку lpFileName, а также размер буфера nBufferLength в символах и адрес буфера lpBuffer, куда записывается в виде си-строки полное имя файла. Функция возвращает длину строки, записанной в буфер, или 0, если произошла ошибка. Кроме того, последний параметр функции — указатель на указатель на си-строку lpFilePart, который используется, чтобы вернуть из функции указатель на последнюю часть имени файла, записанного в буфер.

В случае C++ с помощью ссылок и Стандартной библиотеки можно вообще избежать использования “классических” указателей. Так что “беззвёздочный” C++-программист возможен.

Неограниченный уровень косвенности

Несмотря на ограниченность применения уровня косвенности выше двух, довольно часто встречается то, что можно назвать неограниченным уровнем косвенности или рекурсивным типом данных. Типичный (и простейший) пример — структура данных, называемая “связанный список” linked list .

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

struct Line < Line *prev; string line; >; int main() < Line *last = nullptr; // Чтение строк. for (string line; getline(cin, line);) < Line *new_line = new Line; new_line->prev = last; new_line->line = line; last = new_line; > // Вывод строк в обратном порядке. while (last) < cout line '\n'; Line *old_line = last; last = last->prev; delete old_line; > return EXIT_SUCCESS; >

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

Указатели на функции

Язык C позволяет определять указатели на функции (в указателе хранится адрес точки входа в функцию) и вызывать функции по указателю. Таким образом, можно во время исполнения программы выбирать какая именно функция будет вызвана в конкретной точке, выбирая значение указателя. Язык C++ позволяет создавать также и ссылки на функции, но ввиду того, что ссылка после инициализации не может быть изменена, область применения ссылок на функции весьма узка.

Функцией высшего порядка higher order function называют функцию, принимающую в качестве параметров другие функции. Функции высшего порядка — одно из базовых понятий функционального программирования. Единственная форма функций высшего порядка в C — функции, принимающие указатели на функции. Язык C++ расширяет круг доступных форм функций высшего порядка, но в примерах ниже мы ограничимся возможностями C.

Простой пример использования указателя на функцию — функция, решающая уравнение вида f(x) = 0, где f(x) — произвольная функция. Конкретные функции f можно передавать по указателю. Приведение функций к указателю на функцию и наоборот производится неявно автоматически, поэтому при присваивании указателю адреса конкретной функции можно не использовать оператор взятия адреса & , а при вызове функции по указателю — не использовать оператор разыменования * (поведение, аналогичное поведению с массивами).

/// Тип "правая часть уравнения" -- функция одного действительного параметра. typedef double (*Unary_real_function)(double); /// Точность приближённого решения, используемая по умолчанию. const double Tolerance = 1e-8; /// Алгоритм численного решения уравнения f(x) = 0 на отрезке [a, b] делением отрезка пополам. /// Данный алгоритм является вариантом двоичного поиска. double nsolve(Unary_real_function f, double a, double b, double tol = Tolerance) < using namespace std; assert(f != nullptr); assert(a < b); assert(0. for (auto fa = f(a), fb = f(b);;) < // Проверим значения функции на концах отрезка. if (fa == 0.) return a; if (fb == 0.) return b; // Делим отрезок пополам. const auto mid = 0.5 * (a + b); // середина отрезка if (mid return abs(fa) < abs(fb)? a: b; if (b - a return mid; // Выберем одну из половин в качестве уточнённого отрезка. const auto fmid = f(mid); if (signbit(fa) != signbit(fmid)) < // Корень на левой половине. b = mid; fb = fmid; > else < assert(signbit(fb) != signbit(fmid)); // Корень на правой половине. a = mid; fa = fmid; > > >

Довольно типичной областью применения указателей на функции является связывание источников (регистраторов) некоторых событий, обычно определяемых в составе некоторой библиотеки, и обработчиков событий, предоставляемых пользователем этой библиотеки. Обработчики событий (функции) вызываются автоматически по переданным указателям. Такие функции также называются функциями обратного вызова callback functions или колбеками callbacks . Например, при щелчке мышью по элементу графического интерфейса вызывается функция-обработчик этого события, “зарегистрированная”, путём передачи её адреса библиотеке графического интерфейса.

В качестве простого примера применения функции обратного вызова рассмотрим функцию, занимающуюся поиском набора корней уравнения f(x) = 0 на заданном отрезке. Сама функция будет работать по достаточно простому алгоритму (который, естественно, не гарантирует, что будут найдены все или даже какие-то из существующих на отрезке корней): предполагаем, что есть некая функция, способная найти один корень на отрезке, если он там есть (например, функция nsolve из примера выше). Теперь берём исходный отрезок поиска [a, b] и некоторое значение “шага” step и проходим по этому отрезку с этим шагом, проверяя участки [a + i step, min(b, a + (i + 1)step], i = 0, … пока не пересечём правую границу отрезка. На каждом участке проверяем, являются ли его границы корнями, и есть ли на нём корень (принимает ли функция f разнознаковые значения на границах). В последнем случае используем “решатель” вроде nsolve (переданный по указателю), чтобы найти корень. Каждый найденный корень — это событие, вызываем для него “обработчик” — функцию обратного вызова по указателю report.

/// Тип "решатель уравнения на отрезке" -- функция вроде nsolve, определённой выше. typedef double (*Equation_solver)(Unary_real_function, double a, double b, double tol); /// Тип функции, вызываемой для каждого корня. /// Процесс поиска останавливается, если эта функция возвращает ложь. typedef bool (*Root_reporter)(double); /// Применяет заданный алгоритм поиска корня на отрезке, /// разбивая заданный отрезок [a, b] на отрезки одинаковой длины step (кроме, возможно, последнего). /// Для каждого найденного корня вызывает функцию report (callback-функция). /// Возвращает правую границу пройденного участка (идёт слева направо по заданному отрезку). double repeated_nsolve ( Unary_real_function f, double a, double b, double step, // шаг на отрезке Root_reporter report, double x_tol = TOLERANCE, // чувствительность по аргументу double f_tol = TOLERANCE, // чувствительность по значению функции Equation_solver solver = nsolve ) < assert(x_tol >= 0. && f_tol >= 0.); assert(a 0.); assert(f && report && solver); using namespace std; double left = a, f_left = f(left); bool f_left_zero = abs(f_left) // Корень на левой границе исходного отрезка? if (f_left_zero && !report(left)) return left; while (left != b) < // Правая граница очередного участка. const double right = fmin(b, left + step), f_right = f(right); const bool f_right_zero = abs(f_right) // Корень на правой границе участка? if (f_right_zero && !report(right)) return right; // Есть корень внутри участка? if (!(f_left_zero || f_right_zero) && signbit(f_left) != signbit(f_right)) < const double root = solver(f, left, right, x_tol); if (!report(root)) return root; > // Передвинуть левую границу. left = right; f_left = f_right; f_left_zero = f_right_zero; > return b; >

Следующий пример демонстрирует “двухзвёздное программирование” и использование указателя на функцию для определения порядка сортировки массива строк с помощью стандартной функции qsort .

#include // qsort #include // strcmp #include using namespace std; // Функция сравнения строк. int line_compare(const void *left, const void *right) < // Обращаем словарный порядок, поменяв местами left и right. return strcmp(*(const char**)right, *(const char**)left); > int main() < const char *lines[] < "may the force be with you", "this is it", "so be it", "it is a good day to die", "through the time and space", "the light shines in the darkness" >; // Сортировать: массив, количество элементов qsort(lines, sizeof(lines) / sizeof(lines[0]), // размер элемента, функция сравнения. sizeof(lines[0]), line_compare); // Распечатаем результат сортировки. for (auto line : lines) cout '\n'; return EXIT_SUCCESS; >

Функция qsort является частью Стандартной библиотеки C. Стандартная библиотека C++ предлагает более удобную и эффективную функцию sort (определённую в заголовочном файле ), однако её рассмотрение выходит за пределы темы данного раздела.

Следующий пример является развитием примера со списком из предыдущего подраздела и использует бестиповые указатели, указатели на указатели и указатели на функции для управления “обобщённым” связанным списком в стиле C. Звенья такого списка могут содержать произвольные данные. Основное требование к звеньям списка — наличие в начале звена указателя на следующее звено, фактически каждый предыдущий указатель указывает на следующий.

/// Возвращает ссылку на указатель на следующее звено звена link. void*& next(void *link) < return *(void**)link; > /// Вставляет link перед head и возвращает link (теперь это -- новая голова списка). void* insert_head(void *head, void *link) < next(link) = head; return link; > /// Вычисляет длину списка. size_t size(void *head) < size_t sz = 0; for (; head; head = next(head)) ++sz; return sz; > /// Указатель на функцию, выполняющую удаление звена. using Link_delete = void(*)(void*); /// Удаляет список, используя пользовательскую функцию удаления. void delete_list(void *head, Link_delete link_delete) < while (head) < auto next_head = next(head); link_delete(head); head = next_head; > >

Теперь сама программа, выводящая строки в обратном порядке, упрощается:

/// Звено списка -- одна строка. struct Line < void *prev; string line; >; /// Вывести строку и удалить объект Line. void print_and_delete(void *ptr) < auto line = (Line*)ptr; cout line '\n'; delete line; > int main() < Line *head = nullptr; // Чтение строк. for (string line; getline(cin, line);) < Line *new_line = new Line; new_line->line = line; head = (Line*)insert_head(head, new_line); > // Вывод количества строк -- элементов списка. cout "\nLines: " << size(head) "\n\n"; // Вывод строк в обратном порядке. delete_list(head, print_and_delete); cin.clear(); cin.ignore(); return EXIT_SUCCESS; >

Впрочем, необходимо отметить, что сочетая такие приёмы со средствами C++, выходящими за пределы “чистого” C, вы рискуете нарваться на неопределённое поведение. Низкоуровневые средства требуют особой внимательности, так как компилятор в таких случаях не страхует программиста. В частности, в общем случае нельзя интерпретировать произвольный указатель как void* и наоборот без выполнения приведения типа. А это может произойти неявно, например, в примере выше мы полагаем, что указатель prev, указывающий на объект структуры Line совпадает с указателем на поле prev этого объекта.

Синтаксическая справка

Правило чтения сложных описаний типов

Конструкции, определяющие переменные или вводящие новые типы в языках C и C++, могут порой иметь довольно запутанный вид. Ниже дано правило, помогающее разобраться в смысле сложных конструкций.

  1. Начиная с имени (в случае typedef , в случае using имя находится вне — см. ниже), читать вправо, пока это возможно (до закрывающей круглой скобки или точки с запятой).
  2. Пока невозможно читать вправо, читать влево (убирая скобки).

Некоторые примеры “расшифровки” типов переменных:

// c (влево) константа char (const и char можно поменять местами) const char c; // str (влево) указатель на (влево) константу char (или константный массив из char) const char* str; // str (влево) константный (влево) указатель на константу char const char* const str; // n (вправо) массив (вправо) из 10 (влево) int int n[10]; // n (вправо) массив (вправо) из 10 (влево) указателей на (влево) int int* n[10]; // n (влево) указатель на (вправо) массив из 10 (влево) указателей на int int* (*n)[10]; // n указатель на массив из 10 (влево) указателей на (вправо) функции, не принимающие аргументов, // (влево) возвращающие указатели (влево) на константы типа int const int* (*(*n)[10])();

Разница между typedef и using

Директива typedef объявляет синоним типа. Используется синтаксис определения переменной, к которой добавили ключевое слово typedef , только вместо собственно переменной вводится синоним типа этой как-бы переменной с её именем.

int * p; // переменная: указатель на int typedef int * pt; // имя pt -- синоним типа "указатель на int" pt px; // тоже переменная типа "указатель на int"

В С++11 появилась возможность объявлять синонимы типов с помощью using-директивы в стиле инициализации переменных:

using pointer = type*;

Объявление typedef можно превратить в using-директиву, заменив typedef на using , вставив после using имя типа и знак равно и убрав это имя типа из объявления справа.

// то же, что typedef double (*Binary_op)(double, double); using Binary_op = double (*)(double, double);

Типы, ассоциируемые с массивами

Пусть N — константа времени компиляции и дано определение

float a[N];
  • float — тип элемента;
  • float& — ссылка на элемент, тип результата операции обращения по индексу, например a[0] ;
  • float* — указатель на элемент, например &a[0] ; a и &a автоматически неявно приводятся к float* ;
  • float[N] — формальный тип переменной a ;
  • float(*)[N] — формальный тип указателя на массив a , результат операции взятия адреса &a ;
  • float(&)[N] — тип ссылки на массив a ; a автоматически неявно приводятся к этому типу; так же как сам массив, ссылка на него автоматически приводится к указателю на массив и на его первый элемент.

Типы, ассоциируемые с функциями

Пусть дано объявление

float foo(int, int);
  • float — тип результата, получаемый при вызове функции, например foo(1, 2) ;
  • float(int, int) — формальный тип символа foo — foo не является переменной, так как переменные функционального типа невозможны, и тем не менее, имеет тип;
  • float(*)(int, int) — указатель на функцию, результат &foo ; foo автоматически неявно приводится к этому указателю;
  • float(&)(int, int) — ссылка на функцию; foo автоматически неявно приводится к этому типу; так же как сама функция, ссылка на неё автоматически приводится к указателю на неё же.

Ссылки и ссылочные типы в C++

Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Это пятая статья из серии, список предыдущих статей приведен в конце в разделе 6. Серия ориентирована на программистов, имеющих определенный опыт работы на C++. Эта статья посвящена ссылкам и ссылочным типам в C++.

Термин «ссылка» широко используется и в обыденной жизни, в компьютерных и других науках и поэтому его смысл сильно зависит от контекста использования. В языках программирования под ссылкой понимают небольшой объект, главная задача которого обеспечить доступ к другому объекту, расположенному в другом месте, имеющему другой размер и т.д. Объекты ссылки удобно использовать на стеке, они легко копируются, что позволяет получить доступ к объекту, на который эта ссылка ссылается, из разных точек кода. В той или иной форме ссылки поддерживаются во всех языках программирования. В ряде языков программирования, таких как C#, Java, Pyton и многих других, ссылки, по существу, являются концептуальным ядром.

В C роль ссылок играют указатели, но работать с ними не очень удобно и в C++ появилась отдельная сущность — ссылка (reference). В C++11 ссылки получили дальнейшее развитие, появились rvalue-ссылки, универсальные (передаваемые) ссылки, которые играют ключевую роль в реализации семантики перемещения — одном из самых значительных нововведений C++11.

Итак, попробуем рассказать о ссылках в C++ максимально подробно.

Оглавление

Оглавление

1. Основы

1.1. Определение ссылки

В простейшем случае ссылка определяется так: если T некоторый тип и есть переменная типа T , то переменная типа T со спецификатором & будет ссылкой на эту переменную, если она инициализирована этой переменной.

T x; T &rx = x; // rx это ссылка на x

После этого rx можно использовать в любом контексте вместо x , то есть rx становится псевдонимом x .

Инициализация ссылки обязательна, нулевые ссылки (ссылки на «ничто») не поддерживаются. Изменить переменную, на которую ссылается ссылка, невозможно — связь между ссылкой и переменной «до гробовой доски». Таким образом, ссылка является константной сущностью, хотя формально ссылочный тип не является константным.

В одной инструкции можно определить несколько ссылок, спецификатор & должен быть у каждой из них.

int x = 1, y = 2; int &rx = x, &ry = y;

Последняя инструкция эквивалентна следующим двум инструкциям:

int &rx = x; int &ry = y;

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

using RT = T&;

Также можно использовать более старый способ, через typedef .

typedef T& RT;

После этого ссылки можно определить так:

int x = 1 using RI = int&; RI rx = x;

Обратим внимание на то, что в C++ смысл термина «ссылочный тип» отличается от смысла, который этот термин имеет в языках программирования со сборкой мусора (C#, Java, etc.). В последних он означает типы, экземпляры которых управляются сборщиком мусора, и доступны исключительно через ссылку. Подробнее о ссылочных типах в разделе 5.1.

Можно определить копию ссылки.

T x; T &rx = x; T &rx2 = rx;

После этого на переменную x будут ссылаться две ссылки. Других собственных операций ссылка не поддерживает, все операторы, примененные к ссылке, на самом деле применяются к переменной, на которую она ссылается. Это касается и таких операторов, как = (присваивание), & (получение адреса), sizeof , typeid . Но вот спецификатор decltype , если его применить к ссылке, дает ссылочный тип.

Остановимся подробнее на присваивании. Присваивание ссылок означает присваивание переменных, на которые ссылки ссылаются. Естественно, что тип этих переменных должен поддерживать присваивание.

int x = 1, y = 2; int &rx= x, &ry = y; rx = ry;

Последняя инструкция эквивалентна следующей:

x = y;

Ссылки rx , ry продолжат ссылаться на переменные x , y соответственно, только теперь x будет иметь значение 2. Такое поведение не вполне традиционно, в других языках происходит присваивание самих ссылок, то есть ссылка, являющаяся левым операндом, становится копией ссылки, являющейся правым операндом. (Именно так работает эмулятор ссылки — шаблон класса std::reference_wrapper<> , см. раздел 5.3.) Но в силу неизменяемости ссылок, в C++ такое невозможно.

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

int x = 1; int &rx = x; rx = 33;

Последняя инструкция эквивалентна

x = 33;

1.2. Разновидности ссылок

Выше мы определили ссылки, которые можно назвать простые ссылки. Но есть еще другие разновидности.

1.2.1. Ссылки на константу

Если T некоторый неконстантный и нессылочный тип или псевдоним, то можно определить ссылку на константу.

const T d = ini_expression; const T &rcd = d;

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

using RCT = const T&;

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

using CT = const T; using RCT = CT&;

Сами ссылки теперь можно определить так:

СT d = ini_expression; СT &rcd = d; RCT rcd2 = d;

Через ссылку на константу нельзя модифицировать объект, на которой она ссылается. Это означает, что для встроенных типов через такую ссылку запрещено присваивание, инкремент, декремент, а для пользовательских типов запрещен вызов неконстантных функций-членов.

const int d = 42; const int &rcd = d; rcd = 43; // ошибка

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

const int d = 42; int &rd = d; // ошибка const int &rcd = d; int &rd2 = rcd; // ошибка

А вот инициализировать ссылку на константу неконстантной переменной или простой ссылкой можно.

int x = 42; const int &rcx = x; // OK int &rx = х; const int &rcx2 = rx; // OK

Напомним некоторые правила использования квалификатора const .

Если в одной инструкции объявляется несколько переменных (в том числе ссылок), то const относится ко всем переменным.

const int d1 = 1, d2 = 2; const int &rcd1 = d1, &rcd2 = d2;

Эти инструкции эквивалентны следующим инструкциям:

const int d1 = 1; const int d2 = 2; const int &rcd1 = d1; const int &rcd2 = d2;

Квалификатор const может стоять как до имени типа, так и после.

const int d = 42; const int &rcd = d;

Эти инструкции эквивалентны следующим инструкциям:

int const d = 42; int const &rcd = d;

Некоторые авторы считают последний вариант более правильным и у них есть серьезные аргументы, см. например [VJG]. В данной статье мы будем придерживаться традиционного варианта.

Нельзя быть дважды константным, компиляторы второй квалификатор const игнорируют (иногда с предупреждением).

using CT = const T; using RCT = const CT&;

Второй const игнорируется.

Ссылку на константу можно превратить в обычную ссылку с помощью оператора const_cast<>() , но это в общем случае потенциально опасное преобразование.

const int d = 42; const int &rcd = d; int &rd = const_cast(rcd); // потенциально опасно

Сделаем теперь одно терминологическое замечания. Ссылки на константу часто называют константными ссылками. Это не вполне точно, ссылки сами по себе являются константными сущностями, а вот ссылаться они могут как на константу, так и на не-константу. В случае с указателями мы должны различать эти два варианта константности, а вот в случае ссылок можно проявить некоторую терминологическую небрежность. Об этом пишет, например, Стефан Дьюхэрст [Dewhurst].

1.2.2. Rvalue-ссылки

Rvalue-ссылки — это разновидность ссылок, которая появилась в C++11. Они отличаются правилами инициализации (см. раздел 2.4) и правилами перегрузок функций с такими параметрами (см. раздел 3.1.3). Если T некоторый неконстантный и нессылочный тип или псевдоним, то rvalue-ссылка определяется так:

То есть для их определения используется спецификатор && , а не & .

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

using RVT = T&&;

Компилятор различает также rvalue-ссылки на константу:

const T &&rvc = ini_expression;

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

Требования к ini_expression и другие подробности об rvalue-ссылках в последующих разделах.

1.2.3. Ссылки на массив

Можно определить ссылку на массив.

int a[4]; int(&ra)[4] = a;

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

int a[6]; int(&ra)[4] = a; // ошибка, размеры отличаются

Можно определить ссылку на массив констант.

const int сa[] = ; const int(&rсa)[4] = ca;

Формально существуют rvalue-ссылки на массив, но они практически не используются.

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

using I4 = int[4]; I4 a; I4 &ra = a;

Можно объявить псевдоним ссылки на массив.

using RI4 = int(&)[4];

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

int a[4]; int(&ra)[4] = a; ra[0] = 42; std::cout 

В C++ к массивом применяется правило, называемое сведением (decay, array-to-pointer decay). (Для перевода термина «decay» еще используется слово «низведение», также можно встретить «разложение».) Суть сведения заключается в том, что почти в любом контексте идентификатор массива преобразуется к указателю на первый элемент и информация о размере теряется. Сведение происходит и при использовании массивов в качестве параметров функций. Функции

void Foo(int a[4]); void Foo(int a[]); void Foo(int *a);

не перегруженные функции, это одно и то же.

Ссылки на массив как раз и являются теми средствами, с помощью которых можно обойти сведение. Функция

void Foo(int(&a)[4]);

принимает аргументы типа int[4] , массивы другого размера и указатели для нее не подходят.

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

Это функция, принимающая int и возвращающая ссылку на массив int[4] .

Особенно удобно использовать шаблоны функций с параметром типа ссылка на массив, для которого тип и размера массива выводится компилятором.

template void Foo(T(&a)[N]);

При конкретизации такого шаблона компилятор выводит тип элементов T и размер массива N (который гарантировано больше нуля). В качестве аргументов можно использовать только массивы, указатели будут отвергнуты. Именно этот прием используется при реализации перегруженных версий std::begin() , std::end() , std::size() и других, которые позволяют трактовать обычные массивы как стандартные контейнеры.

1.2.4. Ссылки на функцию

Ссылка на функцию определяется следующим образом:

void Foo(int); void(&rf)(int) = Foo;

Для вызова функции через ссылку используется привычный синтаксис.

void Foo(int); void(&rf)(int) = Foo; rf(42); // тоже самое, что и Foo(42);

Константного варианта ссылки на функцию не существует, так как тип функции не может быть константным. Формально существуют rvalue-ссылки на функцию, но они практически не используются.

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

using FI = void(int); void Foo(int); FI &rf = Foo;

Можно объявить псевдоним ссылки на функцию.

using RFI = void(&)(int);

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

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

Нельзя определить ссылку на функцию-член класса.

1.3. Ссылки и указатели

1.3.1. Взаимозаменяемость

Ссылки были добавлены в C++ в качестве более удобной альтернативы указателям, но указатели и ссылки не являются полностью взаимозаменяемыми.(Конечно, при подобной замене надо корректировать код, синтаксис доступа через ссылку и указатель разный.)

Указатели часто можно заменить ссылкой, но не всегда, так как указатель может иметь значение nullptr и это может оказаться существенным моментом в логике работы программы, когда как ссылки не могут быть нулевыми. Также нельзя создавать массивы ссылок и нет ссылочного аналога нетипизированного указателя void* . Указатели могут оказаться незаменимыми в низкоуровневых решениях, где используется арифметика указателей.

Ссылки также не всегда можно заменить указателями. В C++ классы имеют так называемые специальные функции-члены — копирующий конструктор, копирующий оператор присваивания и их перемещающие аналоги. Эти функции-члены имеют единственный параметр, который обычно имеет ссылочный тип. При перегрузке операторов также часто нельзя обойтись без параметров ссылочного типа, см. раздел 3.1.1. Эти параметры нельза заменить указателями. Rvalue-ссылки также нельзя заменить указателем.

В целом можно рекомендовать по возможности стараться использовать ссылки вместо указателей, так как указатели в значительной степени являются С-архаизмом.

1.3.2. Внутреннее устройство ссылок

Как и многие другие языки программирования, C++ скрывает внутренне устройство ссылок. Получить какую либо информацию об объекте ссылки непросто — любая операция над ссылкой означает операцию над объектом, на который она ссылается.

Достаточно традиционный взгляд — это считать ссылку «замаскированным» константным указателем. Но Страуструп и другие авторы, например Стефан Дьюхэрст [Dewhurst], считают такую точку зрения неверной и настаивают, что ссылка — это просто псевдоним переменой, на которую она ссылается. Компилятор в процессе оптимизации может вообще удалить объекты ссылок. Понятно, что в простых случаях это сделать можно (см. примеры в разделе 1.1), но как обойтись без объекта ссылки при использовании ссылок в качестве параметров и возвращаемых значений функций, членов классов и реализации полиморфизма не вполне понятно. Вот пример, который косвенно подтверждает материальность ссылок.

class X < int &m_R; public: X(int& r) : m_R(r)<>>;

По идее sizeof(X) должен давать размер объекта ссылки. Эксперименты дают ожидаемый результат — этот размер равен размеру указателя.

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

1.4. Разное

1.4.1. Полиморфизм

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

class Base < public: virtual void Foo(); // . >; class Derv : public Base < public: void Foo() override; // . >; Derv d; Base &r1 = d; r1.Foo(); // Derv::Foo() Derv &rd = d; Base &r2 = rd; r2.Foo(); // Derv::Foo()

Операторы static_cast<>() и dynamic_cast<>() можно использовать со ссылками, единственное отличие состоит в том, что если невозможно выполнить приведение dynamic_cast<>() , то при работе с указателями возвращается nullptr , а при работе со ссылками выбрасывается исключение типа std::bad_cast .

1.4.2. Внешнее связывание

Для ссылок можно реализовать внешнее связывание.

// file1.cpp extern int &ExternIntRef; // file2.cpp int ExternInt = 125; int &ExternIntRef = ExternInt;

Скорее всего, особой пользы в этом нет, но формальная возможность есть.

1.4.3. Неполные объявления

В C++ в ряде случаев компилятору для компиляции правильного кода достаточно знать, что то или иное имя является именем какого-то пользовательского типа (класса, структуры, объединения, перечисления), а полное объявление типа не нужно. В этом случае можно использовать неполное объявление (incomplete declaration), называемое еще упреждающим или предваряющим (forward declaration). Типы с неполным объявлением называются неполными.

Использование неполных типов позволяет решить ряд проблем, традиционно свойственных коду, написанному на С++. Можно уменьшить зависимость проекта по заголовочным файлам, что сокращает время компиляции и предупреждает потенциальные конфликты имен. Неполные объявления позволяют разрывать циклические зависимости, реализовывать решения, полностью разделяющие интерфейс и реализацию (непрозрачные указатели).

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

class X; // неполное объявление class Y < X &m_X; public: Y(X& x) : m_X(x)< /* . */ >// . >;

Но другие операции над ссылками невозможны без полного определения типа.

2. Правила инициализации ссылок

Ссылки должны быть обязательно инициализированы. Если ссылка объявлена глобально или в области видимости пространства имен или локально, то она должна быть инициализирована при объявлении (за исключением extern переменных). Для членов класса предназначены специальные правила инициализации, см. далее раздел 2.2.

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

2.1. Синтаксис инициализации

В C++ для инициализации переменной, в том числе и ссылки, можно использовать разные синтаксические конструкции. В данной статье мы в основном используем традиционный вариант с помощью символа = .

int x = 6; int &rx = x;

Единственный контекст, в котором такой синтаксис невозможен — это инициализация нестатического члена класса в списке инициализации конструктора, см. раздел 2.2. Обратим внимание на то, что символ = в данном случае не является оператором присваивания.

Другой вариант — это универсальная инициализация (uniform initialization), которая появилась в C++11. В этом случае используются фигурные скобки.

int x = 6; int ℞

Этот вариант инициализации самый универсальный, он допустим в любом контексте.

Есть еще вариант универсальной инициализации с символом = .

int x = 6; int &rx = ;

Но для инициализации ссылок он синтаксически избыточен. Кроме того, если определять ссылку с использованием ключевого слова auto (см. раздел 2.5), то выводимый тип будет конкретизацией шаблона std::initializer_list<> , что, скорее всего, не будет соответствовать ожиданиям программиста.

Еще один вариант — это использование круглых скобок.

int x = 6; int &rx(x);

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

class X < public: X(); // . >; const X &rx(X());

На первый взгляд rx — это определение переменной типа const X& , инициализированной неименованным экземпляром типа X , это полностью соответствует синтаксису C++. Но эту инструкцию также можно трактовать как объявление функции, которая возвращает const X& и имеет параметр типа указатель на функцию, которая возвращает X и не имеет параметров. В соответствии с вышеупомянутым правилом, компилятор выбирает второй вариант. Конечно, тяжелых последствий это не вызовет, так как сразу же возникнут ошибки компиляции, но потратить время на осмысление ситуации, возможно, придется. Для исправления ситуации можно, например, взять X() в дополнительные скобки.

2.2. Члены класса ссылочного типа

В классе можно объявить члены ссылочного типа. Нестатический член обычно инициализируется в списке инициализации конструктора с использованием параметров конструктора. В C++11 нестатический член можно инициализировать непосредственно при объявлении, но предложить какой-нибудь содержательный пример в данном случае сложно.

class X < int &m_R; public: X(int& r) : m_R(r)< /* . */ >// . >;

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

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

Можно объявить статический член ссылочного типа. Он должен быть инициализирован при определении. В C++17 появилась возможность инициализировать такой член при объявлении, для этого он должен быть объявлен с ключевым словом inline .

сlass X < public: static const int &H; static inline const int &G = 32; // . >; const int &X::H = 4;

2.3. Категория значения

В C++ каждое выражение наряду с типом имеет категорию значения (value category). (И тип и категория значения выражения известны во время компиляции.) Категория значения необходима для описания правил использования ссылок. Первоначально (в C) было только две категории значения — lvalue и rvalue. Lvalue — это именованная переменная (то, что могло находится в левой части присваивания), а rvalue — это временные, неименованные сущности (могут находится в правой части присваивания). Но в процессе развития языка определение категорий значения становится более сложным. Сейчас в C++17 имеется 5 категорий значения, подробнее см. [VJG], есть статья на Хабре, написанная igorsemenov. Для изложения представленного материала нам достаточно использовать упрощенный вариант, включающий lvalue и rvalue.

  1. Именованная переменная (в том числе и rvalue-ссылка).
  2. Результат применения оператора разыменования ( * ).
  3. Результат применения к именованным переменным операторов доступа к членам ( . , -> ) и индексатора.
  4. Строковый литерал.
  5. Вызов функции, которая возвращает ссылку или ссылку на константу.
  1. Результат применения оператора получение адреса ( & ).
  2. Результат применения других операторов (за исключением lvalue п.2 и п.3).
  3. Простой литерал ( 42 , ’X’ , etc.), член перечисления.
  4. Вызов функции, которая возвращает не-ссылку.
  5. Вызов функции, которая возвращает rvalue-ссылку.

Lvalue можно еще разделить на изменяемые и неизменяемые (константные). Rvalue также можно разделить на изменяемые и неизменяемые, но неизменяемые rvalue практически не используются и мы не будем их рассматривать. Обратим внимание на пункты, начинающиеся с «Вызов функции, которая возвращает . ». Под это попадают также приведения типа, в том числе и неявные.

2.4. Требования к инициализирующему выражению

Пусть T некоторый неконстантный и нессылочный тип или псевдоним.

Это простая ссылка. Требования к ini_expression : lvalue типа T , T& , T&& или lvalue/rvalue любого типа, имеющего неявное преобразование к T& .

const T &r = ini_expression;

Это ссылка на константу. Требования к ini_expression : lvalue/rvalue типа T , T& , T&& , const T , const T& или любого типа, имеющего неявное преобразование к одному из этих типов.

Это rvalue-ссылка. Требования к ini_expression : rvalue типа T , T&& или lvalue/rvalue любого типа, имеющего неявное преобразования к T , T&& . Обратим внимание, что ini_expression не может быть именованной переменной ссылочного типа (в том числе и T&& ), то есть прямо rvalue-ссылку скопировать нельзя. Как правильно копировать rvalue-ссылку показано далее в разделе 3.1.4

2.5. Инициализация ссылок с использованием автоопределения типа

Многие современные языки программирования со статической типизацией (то есть определяющие тип переменных на этапе компиляции) имеют возможность не указывать явно тип переменных, а предоставить вывод типа компилятору, который решает эту задачу исходя из типа инициализирующего выражения. В C++11 также появилась такая возможность, для этого используется ключевое слово auto . Но в этом случае правила вывода типа переменной не столь просты, как может показаться с первого взгляда. Ключевое слово auto может быть дополнено спецификатором ссылки и квалификатором const , что усложняет правила вывода и иногда приводит к неприятным неожиданностям. Еще следует обратить внимание на то, что в этом случае при выводе типа переменных не используются неявные преобразования типа, в том числе основанные на правилах полиморфизма. Также с использованием auto нельзя объявлять члены класса. В приводимых примерах T некоторый неконстантный и нессылочный тип или псевдоним.

auto x = ini_expression;

Тип переменной x никогда не будет выведен ссылочным или константным. Тип x выводится как T , если ini_expression имеет тип T , T& , T&& , const T , const T& , категория значения ini_expression может быть любая. В процессе инициализации вызывается копирующий или перемещающий конструктор для типа T . Если ini_expression lvalue, то будет вызван копирующий конструктор, если ini_expression rvalue, то при поддержке типом T семантики перемещения будет вызван перемещающий конструктор, иначе копирующий. В случае rvalue вызов конструктора может быть удален при оптимизации.

auto &x = ini_expression;

Тип переменной x выводится как T& , если ini_expression имеет тип T , T& , T&& . Тип x выводится как const T& , если ini_expression имеет тип const T , const T& . Если выводимый тип T& , то ini_expression должен быть lvalue.

const auto &x = ini_expression;

Тип переменной x выводится как const T& , если ini_expression имеет тип T , T& , T&& , const T , const T& , категория значения ini_expression может быть любая.

auto &&x = ini_expression;

Этот тип ссылки называется универсальной ссылкой (universal reference), и имеет довольно специфические правила вывода, выводимый тип зависит от категории значения ini_expression . Тип переменной x выводится как T& , если ini_expression является lvalue и имеет тип T , T& , T&& . Тип переменной x выводится как const T& , если ini_expression является lvalue и имеет тип const T , const T& . Тип переменной x выводится как T&& , если ini_expression является rvalue и имеет тип T , T& , T&& . В C++17 этот тип ссылки стали называть передаваемой ссылкой (forwarding reference), о причинах рассказано далее в разделе 3.2.4.

Особо следует отметить случай, когда ini_expression является массивом или функцией. В этом случае в определении

auto x = ini_expression;

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

3. Ссылки в качестве параметров и возвращаемого значения функций

На самом деле ссылочные типы в основном используются в качестве типа параметров и возвращаемого значения функций, а не для создания переменных.

3.1. Параметры функций

В этом случае ссылки обеспечивают ряд преимуществ.

  1. Затраты на передачу параметра постоянны и не зависят от типа, на который ссылается ссылка (они эквиваленты затратам на передачу указателя).
  2. Позволяют модифицировать объект, на который ссылается параметр, то есть превращать параметр в выходной.
  3. Позволяют запретить модифицировать объект, на который ссылается параметр.
  4. Обеспечивают реализацию семантики перемещения.
  5. Передача ссылки по стеку вызовов не приводит к появлению висячих ссылок.
  6. Поддерживают полиморфизм.

3.1.1. Специальные функции-члены и перегруженные операторы

В C++ классы имеют так называемые специальные функции-члены — копирующий конструктор, копирующий оператор присваивания и их перемещающие аналоги. Эти функции-члены имеют единственный параметр, который обычно имеет ссылочный тип. При перегрузке операторов также часто нельзя обойтись без параметров ссылочного типа.

class X < public: X(const X& src); // копирующий конструктор X& operator=(const X& src); // оператор копирующего // присваивания X(X&& src) noexcept; // перемещающий конструктор X& operator=(X&& src) noexcept;// оператор перемещающего // присваивания // . >; X operator+(const X& lh, const X& rh); // перегруженный оператор +

Для копирующего конструктора и перемещающих операций тип параметра изменить нельзя. При перегрузке операторов (в том числе и оператора копирующего присваивания) передачу параметра по ссылке иногда можно заменить передачей по значению, см. раздел 3.3.

3.1.2. Требования к аргументам

Рассмотрим особенности использования параметров функций ссылочного типа. В приводимых примерах T некоторый неконстантный и нессылочный тип.

void Foo(T x);

Это передачу параметра по значению. Подробнее см. раздел 3.3. В ряде случаев мы должны сравнивать передачу параметра по значению и передачу параметра по ссылке.

void Foo(T& x);

Параметр — простая ссылка. Требования к аргументу: lvalue типа T , T& , T&& или lvalue/rvalue любого типа, который имеет неявное преобразование к T& . В этом случае мы имеем возможность модифицировать аргумент, то есть x может быть выходным параметром.

void Foo(const T& x);

Параметр — ссылка на константу. Требования к аргументу: lvalue/rvalue типа T , T& , T&& , const T , const T& или любого типа, имеющего неявное преобразование к одному из этих типов. В этом случае мы не имеем возможность модифицировать аргумент.

void Foo(T&& x);

Параметр — rvalue-ссылка. Требования к аргументу: rvalue типа T , T&& или lvalue/ rvalue любого типа, который имеет неявное преобразование к T , T&& . Этот вариант используется для реализации семантики перемещения. В классе, поддерживающем перемещение, должен быть определен перемещающий конструктор с параметром типа rvalue-ссылка и оператор перемещающего присваивания с таким же параметром.

class X < public: X(X&& src) noexcept; X& operator=(X&& src) noexcept; // . >;

Эти функции-члены и выполняют в конечном итоге перемещение. В ряде случаев компилятор сам генерирует перемещающий конструктор и оператор перемещающего присваивания, подробности см. [Meyers]. Использование noexcept не является строго обязательным, но крайне желательным, иначе в стандартной библиотеке в некоторых случаях перемещение будет заменено на копирование, подробности см. [Meyers].

Ключевой момент концепции семантики перемещения заключается в том, что источником перемещения является rvalue и, таким образом, после выполнения перемещения этот объект будет недоступен и не надо беспокоиться о случайном доступе к «опустошенному» объекту. (Возможно принудительное приведение lvalue к rvalue (см. раздел 3.1.4 ), но в этом случае программист уже сам отвечает за недопущение некорректных операций.)

3.1.3. Перегрузка функций

Перегрузка (overloading) — это возможность одновременно использовать несколько функций или шаблонов функций с одним именем. Компилятор различает их благодаря тому, что они имеют разный набор параметров. В точки вызова компилятор анализирует типы аргументов и определяет, какая конкретно функция должна быть вызвана. Эта процедура называется разрешением перегрузки. Разрешение перегрузки может завершиться неудачей, то есть компилятор может не отдать предпочтение ни одной из функций, в этом случае говорят, что вызов неоднозначный (ambigious). Более подробно перегрузка обсуждается в одной из предыдущих статей серии.

Правила разрешения перегрузки имеют важное значение: для того, чтобы реализовать свои замыслы, программист должен четко понимать какая из перегруженных функций будет вызвана в том или ином контексте. В частности семантика перемещения базируется на правилах перегрузки функций, имеющих параметры типа rvalue-ссылка, и неправильно понимание правил перегрузки может привести к тому, что перемещение «молча» будет заменено копированием. Правила перегрузка функций, имеющих параметры ссылочного типа, можно считать расширением правил, изложенных в предыдущем разделе, так как они определяют выбор между несколькими допустимыми вариантами.

Пусть функции перегружены следующим образом:

void Foo(T& x); void Foo(const T& x);

В этом случае для неконстантных lvalue-аргументов будет выбрана первая функция (хотя вторая также допустима), для константных lvalue-аргументов и rvalue-аргументов вторая (первая недопустима).

Пусть функции перегружены следующим образом:

void Foo(T& x); void Foo(T x);

Здесь для константных lvalue-аргументов и rvalue-аргументов будет выбрана вторая функция (первая недопустима), а вот для неконстантных lvalue-аргументов разрешение перегрузки завершится неудачей (хотя обе функции допустимы), то есть для этого варианта перегруженных функций первая функция никогда не будет выбрана.

Пусть функции перегружены следующим образом:

void Foo(const T& x); void Foo(T x);

Для любых аргументов разрешение перегрузки завершится неудачей (хотя обе функции допустимы).

Пусть функции перегружены следующим образом:

void Foo(const T& x); void Foo(T&& x);

В этом случае первая функция будет выбрана для lvalue-аргументов (вторая недопустима), а вторая для rvalue-аргументов (хотя первая допустима). Это правило является ключевым при реализации семантики перемещения, именно оно используется при выборе между копирующим и перемещающим конструктором (а также соответствующими операторами присваивания).

class X < public: X(const X& src); // копирующий конструктор X(X&& src) noexcept; // перемещающий конструктор // . >;

Пусть функции перегружены следующим образом:

void Foo(T& x); void Foo(T&& x);

В этом случае первая функция будет выбрана для неконстантных lvalue-аргументов (вторая недопустима), вторая для rvalue-аргументов (первая недопустима), а для константных lvalue-аргументов обе функции недопустимы и, соответственно, разрешение перегрузки завершится неудачей.

Пусть функции перегружены следующим образом:

void Foo(T x); void Foo(T&& x);

В этом случае первая функция будет выбрана для lvalue-аргументов (вторая недопустима), а вот для rvalue-аргументов разрешение перегрузки завершится неудачей (хотя обе функции допустимы), то есть для этого варианта перегруженных функций вторая функция никогда не будет выбрана.

Отметим также появившееся в С++11 ссылочные квалификаторы для нестатических функций-членов. Они позволяют перегружать по категории значения скрытого параметра this .

class X < public: X(); void Foo() &; // this указывает на lvalue void Foo() &&; // this указывает на rvalue // . >; X x; x.Foo(); // X::Foo() & X().Foo(); // X::Foo() &&

3.1.4. Функции с параметром типа rvalue-ссылка

Рассмотрим случай, когда у нас есть функция с параметром типа rvalue-ссылка. Такая функция принимает только rvalue-аргументы. Пусть теперь этот параметр мы просто должны передать другой функции с параметром типа rvalue-ссылка. В этом случае необходимо учитывать, что сам параметр будет lvalue и поэтому для корректной передачи такой параметр необходимо пропустить через преобразование типа static_cast() или вызов стандартной функции std::move() , которые превращают lvalue в rvalue.

class X; void FooInt(X&& x); void Foo(X&& x) < // x это lvalue, а std::move(x) это rvalue FooInt(std::move(x)); // . >

Если этого не сделать, то будет либо ошибка, либо, если есть перегруженная функция c параметром типа X , X& , const X& , то будет выбрана она (в частности перемещение может быть заменено на копирование, см. раздел 3.1.3). Таким образом, без этого преобразования семантика перемещения в дальнейшем не будет работать. Такие ошибки опасны тем, что их можно долго не замечать.

Обратим внимание на немного сбивающее с толка название std::move() . Реально эта функция ничего не перемещает, это приведение типа, которое превращает lvalue в rvalue и использовать ее надо только так, как показано в примере — ее вызов должен быть аргументом функции, с параметром rvalue-ссылка. Реальное перемещение делает перемещающий конструктор.

3.2. Параметры шаблонов функций

3.2.1. Автоматический вывод аргументов шаблонов функций

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

template void Foo(T x);

Тип аргумента шаблона выводится как T , если аргумент имеет тип T , T& , T&& , const T , const T& , тип параметра x будет T , категория значения аргумента может быть любая. Таким образом, тип T никогда не будет выведен ссылочным или константным. Здесь мы имеем передачу параметра по значению.

template void Foo(T& x);

Тип аргумента шаблона выводится как T , если аргумент имеет тип T , T& , T&& , тип параметра x будет T& , аргумент должен быть lvalue. Тип аргумента шаблона выводится как const T , если аргумент имеет тип const T , const T& , тип параметра x будет const T& , категория значения аргумента может быть любая.

template void Foo(const T& x);

Тип аргумента шаблона выводится как T , если аргумент имеет тип T , T& , T&& , const T , const T& , тип параметра x будет const T& , категория значения аргумента может быть любая.

template void Foo(T&& x);

Это универсальная ссылка. Аргумента шаблона выводится как T& , если аргумент lvalue и имеет тип T , T& , T&& , тип параметра x будет также T& . Аргумента шаблона выводится как const T& , если аргумент lvalue и имеет тип const T , const T& , тип параметра x будет также const T& . Тип аргумента шаблона выводится как T , если аргумент rvalue и имеет тип T , T& , T&& , тип параметра x будет T&& .

Особо следует отметить случай, когда аргумент вызова является массивом или функцией. В этом случае в шаблоне функции

template void Foo(T x);

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

3.2.2. Явное задание аргумента шаблона функции

Аргумент шаблона функции может быть указан явно. Иногда это вынужденное решение, когда автоматический вывод невозможен (например для типа возвращаемого значения) или не дает нужный результат (например для ссылочных типов). В этом случае механизм вывода аргументов шаблона не используется и мы фактически имеем дело с нешаблонной функцией. В частности, явное задание аргумента используется в рассматриваемом далее шаблоне функции std::forward<>() .

Пусть у нас параметр функции имеет тип ссылки на параметр шаблона. В этом случае, если явно заданный аргумент шаблона будет иметь ссылочный тип, то мы получим, что параметр функции будет иметь тип ссылка на ссылку. В C++ такие типы запрещены, поэтому в этой ситуации выполняется операция под названием свертывание ссылок (reference collapsing), в результате чего тип параметра функции будет ссылка или rvalue ссылка. Подробнее свертывание ссылок рассмотрено в раздел 5.2.2, простые примеры будут в следующем разделе.

3.2.3. Универсальные ссылки и rvalue-ссылки

Универсальная ссылка и rvalue-ссылка объявляются одинаково, с помощью спецификатора && , поэтому важно четко понимать, с каким вариантом мы имеем дело в том или ином случае.

Универсальная ссылка — это не особая разновидность ссылок, а некоторый специальный механизм автоматического вывода аргумента шаблона и для того чтобы он был использован, необходимо выполнение трех условий.

  1. Наличие шаблона функции с типовым параметром (обозначим его через T ).
  2. Параметр функции объявлен как T&& .
  3. Аргумент шаблона выводится автоматически, исходя из типа аргумента вызова функции.

Если аргумент шаблона задается явно и параметр шаблона функции объявлен как T&& , то в случае аргумента шаблона ссылочного типа применяется свертывание ссылок (см. раздел 5.2.2) и параметр конкретизированной функции превратится в обычную ссылку или rvalue-ссылку. Если аргумент шаблона нессылочного типа, то параметр будет rvalue-ссылка.

class X < public: X(); // . >; X x; // x это lvalue

Рассмотрим несколько вариантов использования x в качестве аргумента при вызове функции.

void F(X&& x); F(x); // ошибка

В данном случае у нас обычная функция (нарушено условие 1), параметр имеет тип rvalue-ссылка, lvalue-аргумент не подходит.

template void Foo(T&& x) Foo(x); // OK

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

Foo(x); // ошибка

Аргумент шаблона задается явно, параметр в данном случае имеет тип rvalue-ссылка, lvalue-аргумент не подходит.

Foo(x); // OK

Аргумент шаблона задается явно и имеет ссылочный тип, следовательно выполняется свертывание ссылок ( X& && -> X& ). Параметр будет обычной ссылкой, поэтому можно использовать lvalue-аргумент.

template class W < public: W(); void Foo1(T&& x); templatevoid Foo2(U&& x); // . >; W wx; wx.Foo1(x); // ошибка

Тип параметра функции-члена Foo1() определяется явно, при конкретизации шаблона класса W , параметр имеет тип rvalue-ссылка, lvalue-аргумент не подходит.

W wrx; wrx.Foo1(x); // OK

Тип параметра функции-члена Foo1() определяется явно, при конкретизации шаблона класса W , аргумент шаблона класса имеет ссылочный тип, следовательно выполняется свертывание ссылок ( X& && -> X& ). Параметр будет обычной ссылкой, поэтому можно использовать lvalue-аргумент.

W wx; wx.Foo2(x);

Здесь мы имеем шаблон функции-члена. В этом вызове аргумент шаблона функции-члена выводится автоматически, параметр будет универсальной ссылкой, поэтому можно использовать lvalue-аргумент.

Таким образом, если параметр функции выглядит как T&& , то при конкретизации шаблона он может превратиться в обычную ссылку или rvalue-ссылку. Это происходит как при автоматическом выводе аргумента шаблона, так и при явном его задании, но при автоматическом выводе у нас нет ограничений на категорию значения аргумента.

Универсальные ссылки также используются при выводе типа переменных, объявленных с помощью auto && . Это происходит при объявлении переменных (см. раздел 2.5) и параметров лямбда-выражений (см. раздел 3.4.1).

3.2.4. Прямая передача

Теперь рассмотрим ситуацию, когда параметр функции, являющейся универсальной ссылкой, надо передать в другую функцию. Параметр функции всегда будет lvalue и для того, чтобы его корректно передать другой функции, мы должны его преобразовать в rvalue, но только тогда, когда аргумент является rvalue, то есть мы должны сохранить категорию значения аргумента. Если этого не сделать, то могут возникнуть проблемы, описанные в разделе 3.1.4, то есть по существу будет отключена семантика перемещения. В силу того, что аргумент шаблона имеет разный тип в зависимости от категории значения аргумента вызова, задача эта решаема и для этого служит стандартный шаблон функции std::forward<>() . Его надо конкретизировать параметром шаблона и параметр пропустить через вызов этой функции.

class X < public: X(); // . >; void FooInt(const X& x); // для lvalue void FooInt(X&& x); // для rvalue template void Foo(T&& x) < // x это lvalue, а std::forward(x) это // lvalue, если аргумент lvalue и rvalue, если аргумент rvalue FooInt(std::forward(x)); // . > X x; Foo(x); // FooInt(const X& x), lvalue аргумент Foo(X()); // FooInt(X&& x), rvalue аргумент

Эта схема передачи параметра называется прямой (иногда идеальной) передачей (perfect forwarding). Теперь понятно, почему универсальную ссылку стали называть передаваемой (forwarding reference). Универсальные ссылки и прямая передача являются довольно сложной темой со своими «подводными камнями». У Скотта Мейерса [Meyers] можно найти много важных и интересных подробностей на этот счет.

Опять же обратим внимание на то, что шаблон std::forward<>() — это преобразование типа, ничего больше он не делает. Его задача — обеспечить корректную работу правил вызова и перегрузки функций, с учетом категории значения аргумента.

3.2.5. Перегрузка шаблонов функций

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

Правила разрешения можно описать так: рассматриваются конкретизации шаблонов и нешаблонных функций и для них применяются правила из раздела 3.1.3. Когда обе функции одинаковы, применяется следующее правило: если одна из них нешаблонная функция, то будет выбрана она, если обе являются конкретизациями шаблонов, то выбирается конкретизация более специализированного шаблона, если нельзя выбрать более специализированный шаблон, то разрешение перегрузки завершается неудачей. Шаблон с универсальной ссылкой будет считаться менее специализированным, чем шаблон с параметром типа ссылка или ссылка на константу. Отметим, что если какой-либо шаблон не конкретизируется, то он просто исключается из разрешения перегрузки (принцип SFINAE). Приведем примеры.

template void Foo(T&& x); template void Foo(T& x);

Для lvalue-аргументов конкретизации обоих шаблонов одинаковы, но будет выбран второй шаблон как более специализированный. Для rvalue-аргументов конкретизации разные и по правилам раздела 3.1.3 будет выбран первый шаблон.

template void Foo(T&& x); class X; void Foo(X&& x);

Для rvalue-аргументов типа X , X&& конкретизации обоих шаблонов одинаковы и будет выбрана вторая функция как нешаблонная, для остальных аргументов первый шаблон.

Перегрузка шаблонов функций и нешаблонных функций скрывает в себе некоторые потенциальные проблемы. Дело в том, что шаблоны часто оказываются жадными (greedy), они выбираются при разрешении перегрузки чаще, чем, возможно, ожидает программист, так как их конкретизации почти всегда дают точное соответствие для аргументов. Шаблоны функций, которые участвуют в перегрузке, могут, по существу, отключить неявные преобразования аргументов для нешаблонных функций. Подробнее эта тема обсуждается в одной из предыдущих статей серии.

3.3. Передача параметра по ссылке на константу vs передачи по значению

Передача параметра по ссылке на константу и по значению в ряде случаев выступают как конкуренты.

void Foo(const T& x); // передача по ссылке на константу void Foo(T x); // передача по значению

Рассмотрим особенности каждого варианта.

Эти варианты не могут быть перегружены (см. раздел 3.1.3), то есть программист заранее должен выбрать один из них. В любом из них аргумент может быть типа T , T& , T&& , const T , const T& или любого типа, имеющего неявное преобразование к какому-то из этих типов, категория значения аргумента может быть любая. Оба варианта гарантируют неизменяемость аргумента.

Рассмотрим теперь требования к типу T и накладные расходы при передаче параметра. При передаче по ссылке на константу особых требований к типу T нет, копируется ссылка, затраты постоянны и совпадают с затратами по копированию указателя. При передаче по значению для lvalue-аргументов вызывается копирующий конструктор, а для rvalue-аргументов вызывается перемещающий конструктор, если тип T поддерживает семантику перемещение, и копирующий конструктор в противном случае. В C++17 для rvalue-аргументов при передаче по значению в ряде случаев не требуется наличия копирующего или перемещающего конструктора, так как оптимизации, удаляющие вызов конструктора, внесены в стандарт и наличие соответствующего конструктора уже не требуется. В предыдущих версиях C++ требовалось наличие конструктора, даже когда он удалялся при оптимизации.

Напомним, что вызов копирующего конструктора может оказаться весьма дорогостоящей операцией, например, стандартные контейнеры используют так называемое глубокое копирование (deep copying), когда копируются все элементы контейнера. Стоимость вызова перемещающего конструктора также больше, чем копирование ссылки.

Передача по ссылке на константу поддерживает полиморфизм, аргумент может иметь тип производный по отношению к типу параметра, но параметр функции получит «правильный» указатель на таблицу виртуальных функций, такой как у аргумента. При передаче по значению происходит так называемая «срезка» (slicing) и вся информация о производном типе теряется.

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

При перегрузке операторов вместо передачи параметра по ссылке на константу иногда можно использовать передачу по значению. Один из известных примеров, где подобное решение целесообразно — это оператор присваивания, реализованный с помощью идиомы «копирование и обмен».

class X < public: X& operator=(X src); // . >;

Но вот как раз с таким оператором присваивания есть одна потенциальная проблема: вместе с ним нельзя будет использовать оператор перемещающего присваивания, так как для них разрешение перегрузки окончится неудачей для rvalue-аргументов, см. правила перегрузки в разделе 3.1.3.

Традиционные рекомендации для выбора из этих двух вариантов следующие: для простых неполиморфных типов с тривиальным копированием и размером до 8 байт использовать передачу по значению, для остальных типов передачу по ссылке на константу.

Интересные и не совсем традиционные размышления по поводу этих вариантов можно найти у Скотта Мейерса [Meyers].

3.4. Лямбда-выражения

3.4.1. Автоопределение типа параметра

Лямбда-выражения до C++20 не могли быть шаблонами, но в С++14 появилась возможность для типа параметров использовать ключевое слово auto , при этом можно использовать спецификатор ссылки и квалификатор const . Это частично компенсировало отсутствие шаблонов (вывод типа auto и вывод типа аргумента шаблона это практически одно и то же). Например, параметр типа универсальная ссылка в лямбда-выражении можно объявить следующим образом:

[](auto&& x)< /* . */ >

Но тут возникает вопрос — что делать, если надо реализовать прямую передачу? Для прямой передачи мы должны шаблон функции std::forward<>() конкретизировать аргументом шаблона, а его в данном случае нет. Оказывается, вместо аргумента шаблона можно использовать decltype(x) . Для rvalue-аргументов это будет другой тип по сравнению с типом, выводимым для шаблонов с универсальной ссылкой, но, несмотря на это, std::forward<>() будет работать так, как надо для прямой передачи. Таким образом, в качестве передаваемого дальше аргумента надо использовать выражение std::forward(x) . Детали можно найти у Скотта Мейерса [Meyers].

3.4.2. Захват переменной по ссылке

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

int callCntr = 0; auto g = [&callCntr]()< ++callCntr; >; g();

В этом примере переменная callCntr используется в качестве счетчика вызовов.

Захват по ссылке потенциально может привести к появлению висячей ссылки (см. раздел 4), так как замыкания (в нашем примере это g ) можно копировать и потенциально копия может иметь время жизни больше, чем захваченная переменная.

3.5. Возвращаемое значение функции

3.5.1. Варианты использования

Использование ссылок в качестве возвращаемых значений функций таит в себе определенную опасность, могут появиться висячие ссылки, см. раздел 4. Но, не смотря на это, такой прием используется достаточно широко, в том числе и в стандартной библиотеке.
В качестве первого примера рассмотрим итераторы. В стандартном интерфейсе итератора перегруженный оператор * (разыменование) обычно возвращает ссылку на объект, хранимый в контейнере. У некоторых стандартных контейнеров есть еще специальные функции-члены, например, индексатор, front() , back() , которые возвращают ссылку на объект, хранимый в контейнере.
Вызов функции, которая возвращает ссылку, может находиться в левой части оператора присваивания. Это делает код более компактным и читабельным и позволяет использовать перегруженные операторы для пользовательских типов таким же образом, как и для встроенных типов.

std::vector v(2); v.front() = 31; v[1] = 41;

При перегрузке оператора присваивания (и составных операторов присваивания: += , etc.) возвращаемое значение должно быть ссылкой на результат операции, это позволяет строить цепочку присваиваний.

x = y = z;

Еще один пример использования ссылок в качестве возвращаемого значения — это потоки ввода/вывода, где перегруженные операторы >> и

int x, y; std::cout 

3.5.2. Автоопределение типа возвращаемого значения


В C++14 появилась возможность не указывать явно, а выводить тип возвращаемого значения функции. Для этого в качестве типа возвращаемого значения указывается auto, при этом можно использовать спецификатор ссылки и квалификатор const. Правила вывода типа те же, что и при инициализации переменных, объявленных с помощью auto, см. раздел 2.5.


В качестве типа возвращаемого значения можно также указать decltype(auto). В этом случае тип возвращаемого значения выводится как decltype(return_expression). То есть, если return_expression будет иметь ссылочный тип, то таким же будет и тип возвращаемого значения.


Автоопределение типа возвращаемого значения используется в основном в шаблонах функций.



4. Висячие ссылки


Для любой ссылки в широком смысле существует проблема висячей ссылки (dangling reference). Она возникает, когда объект, на который ссылается ссылка, удаляется или перемещается, а ссылка про это «ничего не знает». В этом случае использование ссылки приводит к так называемому неопределенному поведению, то есть может произойти все, что угодно — аварийное завершение программы, неверный, но правдоподобный результат и другие неприятные вещи.


В C++ в ряде случаев компилятор гарантирует отсутствие висячих ссылок, но в общем случае программисту самому приходиться следить, чтобы висячие ссылки не появлялись.


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


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



4.1. Ссылка на rvalue



Как мы видели выше, ссылка на константу и rvalue-ссылка может быть инициализирована rvalue. Спрашивается, а на что она тогда ссылается? Компилятор в этом случае реализует механизм под названием временная материализация (temporary materialization) — создается скрытая переменная, которая инициализируется этим rvalue, и ссылка будет ссылаться на эту переменную. И самое важное, компилятор обеспечивает время жизни этой переменной не меньше, чем время жизни ссылки, поэтому такая ссылка никогда не станет висячей. Следующий странноватый на первый взгляд код является совершенно корректным.


int &&rr = 7; rr = 8;

Литерал 7 — это rvalue, значить происходит временная материализация и во второй инструкции просто меняется значение соответствующей скрытой переменной.

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

4.2. Временные объекты

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

4.3. Примеры

Можно, конечно, висячую ссылку создать как-нибудь так:

int &dx = *new int(32); delete &dx; // dx — висячая ссылка

Но подобный код встречается, наверное, очень редко. Чаще всего проблемы возникают, когда объект удаляется неявно. Рассмотрим насколько типичных сценариев.

Одна из самых грубых ошибок — это возврат из функции ссылки на локальный объект.

class X < public: X(); // . >; X& Foo() < X x; // . return x; >const X& Foo2() < const X &ret = X(); // . return ret; >

Подобный код гарантирует висячую ссылку. (Правда иногда может спасти inline подстановка.) Компилятор выдает предупреждение, но не ошибку.

Рассмотрим теперь функцию:

const X& Foo(const X& x) < // . return x; >

Если при вызове этой функции используется lvalue-аргумент, то гарантируемых проблем не возникает, время жизни x будет определяться контекстом вызова, но в случае rvalue-аргумента время жизни x будет тело функции и после вызова этой функции возвращаемая ссылка будет ссылаться на удаленный объект. Спасти ситуацию может inline подстановка или, если вызов этой функции будет инициализировать значение X , а не ссылку, тогда деструктор X будет вызван после копирования.

Интересно, что подобным образом реализованы некоторые стандартные функции, например:

// https://en.cppreference.com/w/cpp/algorithm/max // header // namespace std template const T& max(const T& a, const T& b);

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

Рассмотрим теперь класс, имеющий ссылку в качестве члена. Такой член должен быть инициализирован в списке инициализации конструктора.

class X; class Y < const X &m_X; // . public: Y(const X& x) : m_X(x)< /* . */>// . >;

Здесь та же проблема, что и в предыдущем примере. Если в конструкторе используется lvalue-аргумент, то все может работать, но если аргумент является rvalue, то m_X гарантируемо будет висячей ссылкой. В этом случае можно подстраховаться и запретить инициализацию экземпляра Y с помощью rvalue, для этого надо объявить конструктор с параметром типа rvalue-ссылка удаленным.

Рассмотрим еще один пример.

class X < int m_Value; public: X(int x) : m_Value(x) <>const int& Value() const < return m_Value; >>;

Рассмотрим первый вариант использования этого класса.

const int &rxv = X(32).Value();

Ссылка на константу rxv инициализируется вызовом функциии, которая возвращает ссылку на константу, а это lvalue (см. раздел 2.3), поэтому временной материализации не будет. Но эта функция является функцией-членом, которая возвращает ссылку на подобъект временного объекта типа X . В соответствии с разделом 4.2 этот временный объект будет удален сразу после того, как ссылка rxv будет инициализирована. Получаем висячую ссылку.

Этот пример показывает потенциально опасную ситуацию — «превращение» rvalue в lvalue с помощью вызова функции, которая возвращает ссылку, и как следствие появления висячей ссылки. Ситуация будет еще более опасной, когда класс имеет неявное преобразование к ссылке, применение неявного преобразования — это по существу вызов функции, но происходит это неявно.

Перепишем предыдущий код следующим образом:

const X &rx = X(32); const int &rxv = rx.Value();

Ссылка на константу rx инициализируется временным объектом типа X , а это rvalue, поэтому происходит временная материализация и rxv будет ссылаться на подобъект «живого» объекта и, таким образом, rxv не будет висячей ссылкой во время жизни rx .

А теперь будем использовать выражение из первого варианта в качестве аргумента функции.

void Foo(const int& rr); Foo(X(32).Value());

В этом примере временной материализации не будет по той же причине, что и в первом варианте, но временный экземпляр X будет удален, только после того, как Foo() вернет управление (см. раздел 4.2) и, таким образом, в теле функции Foo() ссылка rr не будет висячей.

В C++11 можно запретить вызов нестатической функции-члена для rvalue:

const int& Value() const & < return m_Value; >const int& Value() const && = delete;

Здесь мы использовали так называемые ссылочные квалификаторы для нестатических функций-членов. Они позволяют перегружать по категории значения скрытого параметра this (см. раздел 3.1.3).

4.4. Стандартные контейнеры

Стандартный доступ к элементам контейнера осуществляется через итератор. В интерфейсе итератора есть перегруженный оператор * (разыменование), который обычно возвращает ссылку на объект, хранимый в контейнере. Если после получения такой ссылки происходит какая-то операция с контейнером, то эта ссылка может оказаться висячей. Понятно, что для любого контейнера вызов clear() гарантирует, что все ранее полученные ссылки становятся висячими. Вот менее очевидный пример — при добавлении элемента в экземпляр std::vector<> может произойти выделение нового буфера и копировании или перемещение всех старых данных в новый буфер, после чего все ранее полученные ссылки становятся висячими. В документации по стандартной библиотеке можно найти информацию о том, при каких операциях с контейнером гарантируется, что ранее полученные итераторы не станут недействительными.

В стандартных контейнерах также есть другие функции-члены (индексатор, front() , back() , etc.), возвращающие ссылки на элементы, хранимые в контейнере, эти ссылки также могут стать висячими.

4.5. Другие языки

Многие языки программирования озабочены проблемой висячих ссылок. В языках, использующих сборку мусора (C#, Java и многие другие), эта проблема решается так: объект, контролируемый сборщиком мусора, может быть удален только после того, как на него не останется ссылок, а при перемещении объекта ссылки автоматически корректируются.

Другой пример — это Rust. Одна из рекламируемых особенностей этого языка является более сложная система отслеживания жизненного цикла ссылок и, возможно, некоторые из описанных выше проблем были бы обнаружены на стадии компиляции.

5. Ссылочные типы и шаблоны

5.1. Ссылочные типы

Если T некоторый нессылочный тип или псевдоним, то T& будет соответствующим ссылочным типом. Можно объявить псевдоним ссылочного типа

using RT = T&;

или с использованием традиционного typedef

typedef T& RT;

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

Ссылки на константу представляют отдельный ссылочный тип:

using RCT = const T&;

Если T константный тип, то в этом объявлении const игнорируется. (Нельзя быть дважды константным.)

Rvalue-ссылки также представляют отдельные ссылочные типы:

using RVT = T&&;

Ссылочные типы являются практически полностью скрытыми, в том смысле, что любой запрос по поводу этого типа (например sizeof или typeid ) будет переадресован к типу, на который этот ссылочный тип ссылается. Размер самой ссылки можно узнать только косвенно, см. раздел 1.3.2. Из-за этих особенностей у ссылочных типов имеются ряд ограничений.

Нельзя объявить указатель на ссылку.

T x; T &rx = x; using RT = T&; RT *prx = rx; // ошибка using PRT = RT*; // ошибка

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

Но можно объявить ссылку на указатель.

T x; T *px = &x; using PT = T*; PT &rpx = px; using RPT = PT&;

Нельзя определить ссылку на ссылку.

T x; T &rx = x; using RT = T&; RT &rrx = rx; // ошибка

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

using RT = T&; using RRT = RT&; // OK

На самом деле типом RRT будет T& , почему это так будет объяснено далее в разделе 5.2.2.

Нельзя объявить массив ссылок. Если мы попробуем как-нибудь так

int x = 1, y = 2; int &ra[] = ; // ошибка

то компилятор выдаст ошибку. Это вытекает из предыдущих ограничений — тип, используемый для определения массива, должен иметь соответствующий тип указателя. Кроме того, оператор sizeof , примененный к элементу массива, должен давать размер элемента, а в случае массива ссылок он будет давать не размер самой ссылки, а размер объекта, на который эта ссылка ссылается.

Нельзя объявить ссылку на void .

using RVOID = void&; // ошибка

5.2. Аргументы шаблона ссылочного типа

В общем случае запрета на использование ссылочного типа в качестве аргумента шаблона нет, но в силу его особенностей тот или иной шаблон может не конкретизироваться с такими аргументами или, что еще опаснее, конкретизироваться, но работать неправильно. Другая проблема — это то, что при автоматическом выводе аргумента шаблона ссылочные типы выводятся только для параметров с универсальными ссылками. Это вынуждает использовать явное задание аргумента шаблона или использовать некоторые специальные «хитрости» (см. раздел 5.3.2).

5.2.1. Свойства типов

При разработке шаблонов широко используются специальные стандартные шаблоны, которые называются свойствами типов (заголовочный файл ). Среди них есть несколько, предназначенных работать со ссылочными типами. Прежде всего, это std::is_reference<> , его статический член value будет иметь значение true , в случае аргументов шаблона ссылочного типа. (На самом деле есть еще свойства типа для более тонкой проверки: std::is_lvalue_reference<> , std::is_rvalue_reference<> .) Отметим, что для остальных свойств типов из этой группы ( std::is_const<> , std::is_integral<> , etc.) это значение будет false и не будет зависеть от типа, на который ссылочный тип ссылается. Также можно использовать шаблон std::remove_reference<> , который превращает ссылочный тип в соответствующий нессылочный (типовой член шаблона type ). Шаблон std::decay<> также снимает ссылочность, но выполняет еще и другие операции над типом.

5.2.2. Свертывание ссылок

Как уже упоминалось выше, ссылки на ссылку не существует, но при использовании аргументов шаблона ссылочного типа в ряде контекстов могут появляться конструкции, которые по правилам C++ интерпретируются как ссылки на ссылку. В этом случае применяется особое правило, которое называется свертывание ссылок (reference collapsing). В результате такая конструкция интерпретируется как ссылка или rvalue-ссылка на нессылочный тип. Правило простое — если обе ссылки являются rvalue-ссылками, то результирующая ссылка также будет rvalue-ссылка, в противном случае результирующая ссылка будет обычная ссылка. (На самом деле правило немного сложнее, нужно еще учитывать константность, см. [VJG].)

Первый пример — это вывод типа параметров функций при явном задании аргумента шаблона (см. также раздел 3.2.3).

template class W < public: W() = default; void Foo(T&& x); // . >; class X < /* . */ >; W wx; // void Foo(X&&); // нет свертывания W wrx; // void Foo(X&); // X& && -> X& W wrvx; // void Foo(X&&); // X&& && -> X&&

Другой пример — это объявление псевдонимов.

using RI = int&; using RRI = RI&; // int& & -> int& using RI = int&; using RRI = RI&&; // int& && -> int& using RI = int&&; using RRI = RI&&; // int&& && -> int&&

Аналогичным образом работают правила объявления псевдонимов с помощью typedef .

Свертывание ссылок появилось в C++03 и было доработано в C++11, подробнее см. [VJG], [Meyers].

5.2.3. Запрет на использование ссылочных типов

Программист при разработке шаблона должен заранее решить вопрос о допустимости аргументов ссылочного типа. Если допустимы, то, возможно, придется использовать специальною обработку для таких аргументов. Если недопустимы, то надо запретить их использование. Наиболее современный вариант — это использование концептов, появившихся в C++20.

template requires (!std::is_reference_v) class X < /* . */ >;

В более старых версиях можно использовать static_assert() .

В стандартной библиотеке аргументы шаблона ссылочного типа для некоторых шаблонов запрещены, например, для контейнеров. В качестве примера шаблонов, где аргументы шаблона ссылочного типа допустимы, можно привести std::pair<> , std::tuple<> .

5.3. Стандартный эмулятор ссылок

В этом разделе описывается шаблон класса std::reference_wrapper<> . Этот шаблон позволяет создать «нормальный» тип, у которого нет ограничений ссылочного типа, но интерфейс максимально к нему близок. Такой шаблон можно назвать эмулятором ссылки.

5.3.1. Как устроен

Пусть T аргумент шаблона, то есть тип, которым конкретизируют шаблон. Традиционная реализация — это обертка над указателем на T . Понятно, что T не может быть ссылочным типом, указатели на ссылку запрещены. А вот константным типом может. Аргумент конструктора — это lvalue типа T , T& . Понятно, почему не rvalue, в этом случае мы бы сразу получили висячую ссылку. Висячую ссылку можно получить и для lvalue-аргумента, это зависит от времени жизни экземпляра класса по сравнению со временем жизни аргумента конструктора. Если не использовать автоматический вывод аргумента шаблона (C++17), то в качестве аргумента конструктора также можно использовать lvalue /rvalue любого типа, имеющего неявное преобразование к T& . Конструктора по умолчанию и, соответственно, возможности создать нулевую ссылку нет. Класс не поддерживает управление жизненным циклом объекта, на который указывает указатель, — деструктор ничего не делает. Семантика копирования — по умолчанию, просто копируется указатель. Перемещающее копирование не поддерживается, так как нет нулевых ссылок. Семантика присваивания — по умолчанию, происходит присваивание указателей. Обратим внимание, что эта семантика отличается от семантики присваивания для ссылок — присваивание ссылок реализовано, как присваивание объектов, на которые они ссылаются. Класс имеет неявное преобразование к T& . Это позволяет использовать экземпляры класса для инициализации ссылок на T и в качестве аргумента в функциях, в которых принимаются ссылки на T .

void Foo(int& rx); int x = 6; std::reference_wrapper rwx = x; int &rx = rwx; // OK Foo(rwx); // OK

А вот изменить значение, на которое ссылается экземпляр, с помощью присваивания или вызвать функцию-член класса T нельзя. Для решения этой задачи надо сначала вызвать функцию-член get() , которая возвращает T& .

int x = 6; std::reference_wrapper rwx = x; rwx = 32; // ошибка rwx.get() = 32; // OK

Тип T может быть типом функции. На этот случай в классе перегружен оператор () .

void Foo(int x); std::reference_wrapper rwf = Foo; rwf(32);

Для создания экземпляра класса можно использовать шаблон функции std::ref<>() , который может выводить аргумент шаблона класса.

int x = 6; auto rwx = std::ref(x); // то же, что и // std::reference_wrapper rwx = x;

Также можно использовать шаблон функции std::сref<>() . В этом случае аргумент шаблона класса выводится как константный тип.

const int x = 6; auto сrwx = std::сref(x); // то же, что и // std::reference_wrapper crwx = x;

5.3.2. Использование

Конкретизации шаблона std::reference_wrapper<> являются «нормальными» типами, их можно использовать для создания массивов, в качестве аргументов для стандартных контейнеров и других шаблонов. Если у нас есть шаблон функции

template void Foo(T param);

то при использовании аргумента, являющегося конкретизацией шаблона std::reference_wrapper<> , мы по существу заменяем передачу параметра по значению на передачу параметра по ссылке. Гарантии того, что с таким аргументом шаблон будет успешно конкретизирован, нет, но определенные доработки шаблона могут решить эту проблему, см. примеры ниже.

В стандартной библиотеке иногда применяется следующий прием: если аргумент шаблона функции имеет тип std::reference_wrapper , то он преобразуется в T& , в противном случае остается неизменным. Приведем примеры.

int x = 1, y = 2; auto rp1 = std::make_pair(std::ref(x), std::ref(y));

Тип rp1 будет выведен, как std::pair .

Того же эффекта можно достичь при использовании конструктора и непосредственно указав аргументы шаблона как ссылочные.

int x = 1, y = 2; auto rp2 = std::pair(x, y);

Тип rp2 также будет выведен, как std::pair . Получилось даже еще и короче, но мы вынуждены явно задавать аргументы шаблона класса, автоматический вывод здесь работать не будет.

Такой способ преобразования аргумента шаблона не получается сам собой, он требует специальных дополнительных усилий от разработчика шаблона функции. Вот цитата из MSVS2019.

// header // namespace std // // ALIAS TEMPLATE _Unrefwrap_t template struct _Unrefwrap_helper < // leave unchanged if not a reference_wrapper using type = _Ty; >; template struct _Unrefwrap_helper> < // make a reference from a reference_wrapper using type = _Ty&; >; // decay, then unwrap a reference_wrapper template using _Unrefwrap_t = typename _Unrefwrap_helper>::type;

По такому же принципу реализован шаблон функции std::make_tuple() .

Шаблон std::reference_wrapper<> может оказаться полезным при разработке других шаблонов, но возможность его использования должна быть предусмотрена заранее.

6. Список статей серии «C++, копаем вглубь»

7. Итоги

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

2. Категория значения выражения — важнейшее понятие, необходимое для описания правил использования ссылок. Имеются две основные категории значения — lvalue и rvalue.

3. Rvalue-ссылки — это разновидность ссылок, которая появилась в C++11. С помощью них реализуется семантика перемещения — одно из самых значительных нововведений C++11.

4. Универсальные (передаваемые) ссылки и прямая передача — это механизм, позволяющий сохранить категорию значения аргумента при сквозной передаче. Это необходимо для поддержки семантики перемещения в шаблонах.

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

6. В C++ нет универсального механизма предотвращения появления висячих ссылок. Эта задача ложится на плечи программиста. Потенциальный источник висячих ссылок — функции, возвращающие ссылку, и члены класса ссылочного типа, с ними надо быть особенно аккуратным.

7. Ссылочные типы имеют ряд ограничений, их использование в качестве аргументов шаблона в общем случае не запрещено, но в отдельных случаях может вызвать проблемы. По этой причине некоторые шаблоны вынуждены ввести запрет на использование ссылочных типов в качестве аргументов шаблона. Для решения проблем, связанных с ограничениями ссылочных типов, может оказаться полезным шаблон класса std::reference_wrapper<> .

Список литературы

[VJG]
Вандевурд, Дэвид, Джосаттис, Николаи М., Грегор, Дуглас. Шаблоны C++. Справочник разработчика, 2-е изд.: Пер. с англ. — СПб.: ООО «Диалектика», 2020.

[Dewhurst]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.

[Meyers]
Мейерс, Скотт. Эффективный и современный C++: 42 рекомендаций по использованию C++11 и C ++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *