Ссылки и ссылочные типы в C++
Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Это пятая статья из серии, список предыдущих статей приведен в конце в разделе 6. Серия ориентирована на программистов, имеющих определенный опыт работы на C++. Эта статья посвящена ссылкам и ссылочным типам в C++.
Термин «ссылка» широко используется и в обыденной жизни, в компьютерных и других науках и поэтому его смысл сильно зависит от контекста использования. В языках программирования под ссылкой понимают небольшой объект, главная задача которого обеспечить доступ к другому объекту, расположенному в другом месте, имеющему другой размер и т.д. Объекты ссылки удобно использовать на стеке, они легко копируются, что позволяет получить доступ к объекту, на который эта ссылка ссылается, из разных точек кода. В той или иной форме ссылки поддерживаются во всех языках программирования. В ряде языков программирования, таких как C#, Java, Pyton и многих других, ссылки, по существу, являются концептуальным ядром.
В C роль ссылок играют указатели, но работать с ними не очень удобно и в C++ появилась отдельная сущность — ссылка (reference). В C++11 ссылки получили дальнейшее развитие, появились rvalue-ссылки, универсальные (передаваемые) ссылки, которые играют ключевую роль в реализации семантики перемещения — одном из самых значительных нововведений C++11.
Итак, попробуем рассказать о ссылках в C++ максимально подробно.
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;
Подробнее о ссылочных типах в разделе 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-ссылка определяется так:
T &&rv = ini_expression;
То есть для их определения используется спецификатор &&, а не &.
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[] = {1, 2, 3, 4};
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 << ra[0];
В 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(&Foo(int x))[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){ /* ... */ }
// ...
};
Но другие операции над ссылками невозможны без полного определения типа.
Ссылки должны быть обязательно инициализированы. Если ссылка объявлена глобально или в области видимости пространства имен или локально, то она должна быть инициализирована при объявлении (за исключением 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 &rx{x};
Этот вариант инициализации самый универсальный, он допустим в любом контексте.
Есть еще вариант универсальной инициализации с символом =.
int x = 6;
int &rx = {x};
Но для инициализации ссылок он синтаксически избыточен. Кроме того, если определять ссылку с использованием ключевого слова 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.
Lvalue:
- Именованная переменная (в том числе и rvalue-ссылка).
- Результат применения оператора разыменования (
*). - Результат применения к именованным переменным операторов доступа к членам (
.,->) и индексатора. - Строковый литерал.
- Вызов функции, которая возвращает ссылку или ссылку на константу.
Rvalue:
- Результат применения оператора получение адреса (
&). - Результат применения других операторов (за исключением lvalue п.2 и п.3).
- Простой литерал (
42,’X’, etc.), член перечисления. - Вызов функции, которая возвращает не-ссылку.
- Вызов функции, которая возвращает rvalue-ссылку.
Lvalue можно еще разделить на изменяемые и неизменяемые (константные). Rvalue также можно разделить на изменяемые и неизменяемые, но неизменяемые rvalue практически не используются и мы не будем их рассматривать. Обратим внимание на пункты, начинающиеся с «Вызов функции, которая возвращает …». Под это попадают также приведения типа, в том числе и неявные.
2.4. Требования к инициализирующему выражению
Пусть T некоторый неконстантный и нессылочный тип или псевдоним.
T &r = ini_expression;
Это простая ссылка. Требования к 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& или любого типа, имеющего неявное преобразование к одному из этих типов.
T &&r = ini_expression;
Это 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;
Этот тип ссылки называется универсальной ссылкой (univercal 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.1. Параметры функций
В этом случае ссылки обеспечивают ряд преимуществ.
- Затраты на передачу параметра постоянны и не зависят от типа, на который ссылается ссылка (они эквиваленты затратам на передачу указателя).
- Позволяют модифицировать объект, на который ссылается параметр, то есть превращать параметр в выходной.
- Позволяют запретить модифицировать объект, на который ссылается параметр.
- Обеспечивают реализацию семантики перемещения.
- Передача ссылки по стеку вызовов не приводит к появлению висячих ссылок.
- Поддерживают полиморфизм.
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);
В этом случае для не
