Перегрузка в C++. Часть I. Перегрузка функций и шаблонов

?v=1

C++ сложный и интересный язык, совершенствоваться в нем можно чуть ли не всю жизнь. В какой-то момент мне захотелось изучать его следующим образом: взять какой-то аспект языка, возможно довольно узкий, и разобраться с ним максимально глубоко и подробно. Такой подход в значительной степени был стимулирован замечательными книгами Скотта Мейерса, Герба Саттера и Стефана Дьюхэрста. Когда накопилось определенное количество материалов, решил познакомить с ними Хабровчан. Так появилась эта серия, которую я назвал «C++, копаем в глубь». Серия помечена как Tutorial, но ориентирована она все-таки не на начинающих, а скорее на intermediate уровень. Первая тема — это перегрузка в C++. Тема оказалась очень обширной и получилось три статьи. Первая статья посвящена перегрузке функций и шаблонов, вторая перегрузке операторов и третья перегрузке операторов управления памятью. Итак начнем копать.


Оглавление

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

Перегрузка поддерживается многими языками программирования, мы будем рассматривать только C++17.


1.1. Перегруженные функции

Функции (а также шаблоны функций) называются перегруженными (overloaded), если они объявлены в одной области видимости (scope) и имеют одно и то же имя. Перегруженные функции не могут иметь разные типы возвращаемого значения, спецификатор исключений или спецификатор удаленной функции (=delete) при одинаковых параметрах.

void Foo();
char Foo(); // ошибка
void Foo(int x);
void Foo(int x) noexcept;  // ошибка
void Foo(double x);
void Foo(double) = delete; // ошибка

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

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

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

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

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

Для параметров, передаваемых по значению, удаляется квалификатор const (и volatile), поэтому

void Foo(int x);
void Foo(const int x);

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


1.2. Общая схема алгоритма поиска функции

В общих чертах алгоритм поиска функции можно описать следующим образом. На первом этапе компилятор осуществляет поиск (lookup) тех перегруженных функций, которые по правилам языка допустимы для данного вызова (candidate functions). В случае шаблонов выполняется еще вывод аргументов шаблона (template argument deduction). У этих функций количество параметров должно совпадать с количеством аргументов и тип аргументов должен совпадать с типом параметров (или существовать неявное преобразование типа аргументов к типу параметров). Если таких функций не найдено, поиск завершается ошибкой. Если найдена ровно одна функция, то поиск завершается успешно. Если найдено несколько функций, то начинается следующий этап, компилятор пытается выбрать ту, которая подходит «лучше всего» для данных аргументов (match the arguments most closely). Этот этап называется разрешением перегрузки (overload resolution). Если такая функция найдена, то разрешение перегрузки завершается успешно, иначе возникает ошибка (ambiguous call to overloaded function). Рассмотрим пример:

void Foo(float x);
void Foo(double x);

Для вызова Foo("meow") ни одной подходящей функции не найдено, для вызова Foo(42) подходят обе функции, компилятор не может выбрать наиболее подходящую, а вот вызовы Foo(3.14f) и Foo(3.14) разрешаются успешно.

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

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

Но успешное разрешение перегрузки — это еще не все. После разрешения перегрузки производится проверка на доступность выбранной функции в точке вызова (то есть не является ли она private или protected). В случае успеха производится проверка на удаленность (то есть не объявлена ли она как =delete). Если эти проверки не проходят, компиляция завершается с ошибкой. Обратим внимание на то, что эти проверки никак не влияют на процедуру разрешения перегрузки, они всегда выполнятся после.


1.3. Текущая область видимости и разрешение перегрузки во вложенных областях видимости

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

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


1.3.1. Выбор текущей области видимости

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

class X {/* ... */};
// ...
X x;
x.Foo();
X::Foo(42);

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

namespace N {/* ... */}
// ...
N::Foo();

В этом случае первоначальной текущей областью видимости будет пространство имен N, переход в объемлющие области вообще не выполняется.

::Foo();

В этом случае первоначальной текущей областью видимости будет глобальное пространство имен, объемлющих областей видимости нет.

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

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

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

Пусть такой вызов находится в блоке

{
    Foo();
}

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


1.3.2. Разрешение перегрузки в классах

Рассмотрим пример.

class B
{
// ...
public:
    void Foo(int x);
};

class D : public B
{
// ...
public:
    D();
    void Foo(double x);
};
// ...
D d;
d.Foo(42);

Какая из двух доступных Foo, будет выбрана? Правильный ответ D::Foo(double), хотя B::Foo(int) подходит лучше и доступна в точке вызова. Поиск начинается с текущей области видимости (в данном случае класс D), найдена функция с соответствующим именем, объемлющая область видимости (в данном случае класс B) не рассматривается. Единственная найденная функция D::Foo(double) может быть вызвана с данным аргументом и разрешение перегрузки завершается успешно. Если бы D::Foo(double) была бы объявлена закрытой или защищенной или удаленной, то компиляция завершилась бы ошибкой, но B::Foo(int) все равно бы не рассматривалась, хотя она и доступна в точке вызова. И только, если из класса D совсем убрать Foo, то компилятор сделал бы текущей областью видимости класс B и выбрал бы B::Foo(int).

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


1.3.3. Локальное объявление функций

Рассмотрим теперь одну редко используемую особенность C++, которая называется локальные объявления функций. Функции можно объявлять локально (в блоке), например:

{
    void Foo();
    void Foo(int x);
// ...
    Foo(42);
// ...
}

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


1.4. Расширение области видимости для разрешения перегрузки

Область видимости для разрешения перегрузки можно расширить с помощью using-объявления и using-директивы. Также, в определенном случае, компилятор самостоятельно расширяет область видимости для разрешения перегрузки.

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


1.4.1. Использования using-объявления в классе

Вот как это делается для предыдущего примера:

class B
{
// ...
public:
    void Foo(int x);
};
class D : public B
{
// ...
public:
    using B::Foo;
    void Foo(double x);
};
// ...
D d;
d.Foo(42);

После этого в разрешении перегрузки будут участвовать перегруженные Foo из области видимости класса B, и компилятор выберет B::Foo(int).


1.4.2. Использования using-объявления локально и в пространстве имен

Использовать using-объявление с использованием имени класса можно только в области видимости производного класса, как показано в предыдущем разделе. Локально или в области видимости пространства имен можно использовать using-объявления с использованием имени пространства имен. Но надо быть внимательным, так как такое объявление будет скрывать соответствующие имена из объемлющей области видимости и, если неудачно выбрать область видимости для размещения using-объявления, то можно получить не расширение, а нежелательное изменение области видимости для разрешения перегрузки. Пример:

namespace N
{
    void Foo(int x);
}

void Foo(const char* x);
// ...
void Test()
{
    using N::Foo; // скрывает Foo(const char*)
    Foo(42);      // OK, N::Foo(int x)
    Foo("meow");  // ошибка, Foo(const char*) скрыта
}

В данном случае, для того чтобы обе версии Foo участвовали в разрешении перегрузки, using-объявление надо размещать так:

namespace N
{
    void Foo(int x);
}

void Foo(const char* x);
// ...
using N::Foo;
void Test()
{
    Foo(42);      // OK, N::Foo(int)
    Foo("meow");  // OK, Foo(const char*)
}


1.4.3. Использования using-директивы

Пусть у нас есть некоторое пространство имен N. Инструкция

using namespace N;

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

Если используется анонимное пространство имен, то функции, объявленные в нем, будут участвовать в разрешении перегрузки вместе с одноименными функциями, объявленными в объемлющем пространстве имен. (Фактически анонимное пространство имен доступно через скрытую using-директиву.)

namespace
{
    void Foo(int x);
}

void Foo(const char* x);
// ...
Foo(42);     // OK, Foo(int), анонимное пространство имен
Foo("meow"); // OK, Foo(const char*)


1.4.4. Поиск, зависимый от типа аргументов

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

namespace N
{
    class X {/* ... */};
    void Foo(const X& x);
}

Рассмотрим код (вне пространства имен N):

N::X x;
Foo(x);

В этом случае при разрешении перегрузки компилятор подключит пространство имен N и, если не будет конфликта с текущей областью видимости, будет выбрана N::Foo(const X&). Это и называется поиском, зависимым от типа аргументов (argument depended lookup, ADL), называемый еще поиском Кёнига. ADL играет важную роль при перегрузке операторов, функций стандартной библиотеки и в других случаях.

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


2.1. Неявные преобразования типа и параметры «близкого» типа

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

Одним из проблемных типов является bool. Для совместимости с С существует неявное преобразование bool в int и неявное преобразование любого числового типа и указателя в bool. Это может породить много трудно обнаруживаемых ошибок. Но в простых случаях при разрешении перегрузки bool четко отделяется от int.

void Foo(int x);
void Foo(bool x);
// ...
int x = 6, y = 5;
Foo(x == y); // Foo(bool)
Foo(x = y);  // Foo(int)

Но для таких перегруженных функций

void Foo(bool x, int y);
void Foo(int x, bool y);

неоднозначный вызов уже сделать легче, вот пример:

Foo(1, 2);

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

enum Qq { One = 1, Two };
void Foo(int x);
void Foo(Qq x);
// ...
Foo(One); // Foo(Qq)
Foo(42);  // Foo(int)

Семантически и побитово совпадающие типы, например, int и long также различаются при разрешении перегрузки.

void Foo(int x);
void Foo(long x);
// ...
Foo(42);  // Foo(int)
Foo(42L); // Foo(long)

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

void Foo(long x);
void Foo(long long x);

void Foo(float) = delete;
void Foo(double) = delete;
void Foo(long double) = delete;


2.2. Нулевой указатель

В C++11 был введен новый тип — std::nullptr_t с единственным значением nullptr. Это позволило избежать потенциальных проблем связанных с неявным преобразованием литерального нуля к типу указателя.

void Foo(int x);
void Foo(void* x);
// ...
Foo(0);       // Foo(int)
Foo(nullptr); // Foo(void*)

В C++98 приходилось писать

Foo((void*)0);

Но это еще не все преимущества. Так как nullptr имеет свой собственный тип, можно перегружать функции по значению nullptr.

void Foo(void* x);
void Foo(std::nullptr_t);
// ...
void* x = nullptr;
Foo(x);       // Foo(void*)
Foo(nullptr); // Foo(std::nullptr_t)

Подобные перегрузки используются в интерфейсе стандартных интеллектуальных указателей.


2.3. Универсальная инициализация и списки инициализации

В С++11 появилась концепция универсальной инициализации с использованием фигурных скобок и добавлен новый шаблон — std::intializer_list<>. Как это часто бывает, возникли некоторые неоднозначности, которые пришлось устранять с помощью дополнительных правил. По существу эти правила относятся к правилам разрешения перегрузки для конструкторов. Вот эти правила.


  1. Пустые фигурные скобки — {}, означают выбор конструктора по умолчанию. Если его нет, то возникает ошибка.
  2. Для непустого списка в фигурных скобках сначала ищется конструктор, с параметром типа std::intializer_list<>. Если его нет или элементы списка не подходят для std::intializer_list<>, ищется другой конструктор, подходящий для элементов списка. Но если элементы списка можно преобразовать к типу, требуемому std::intializer_list<>, с помощью неявного сужающего преобразования (то есть преобразования с потерей точности числовых данных), то возникает ошибка.

Рассмотрим несколько примеров для стандартного вектора std::vector, который имеет конструктор с параметром типа std::intializer_list.

Вот первый пример:

std::vector v1(3, 1), v2{3, 1};

В этом случае v1 — это вектор размера 3 с элементами равными 1. v2 — это вектор размера 2 с элементами равными 3 и 1, выбирается конструктор с параметром типа std::intializer_list, хотя есть и другой конструктор, принимающий элементы списка.

Другой пример:

std::vector u1(3, "meow"), u2{3, "meow"};

В этом случае u1 и u2 одинаковы, векторы размера 3. Для u2 элементы списка не подходят для конструктора с параметром типа std::intializer_list и поэтому выбирается конструктор тот же, что и для u1.

И третий пример:

std::vector b1(3, true), b2{3, true};

В этом случае b1 вектор размера 3 с элементами, равными true. А вот b2 не компилируется, так как для конструктора с параметром типа std::intializer_list требуется сужающее преобразование от int к bool.

Подробнее про универсальную инициализацию можно почитать у Скотта Мейерса [Meyers2].


2.4. Функции с переменным числом параметров

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


2.5. Шаблоны функций

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

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


2.5.1. Общие правила перегрузки

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

Если выбрана конкретизация шаблона, то проверяется, нет ли полной специализации этого шаблона для выведенного типа аргумента конкретизации. Если такая специализация есть, то выбирается она. Обратим внимание на то, что полные специализации рассматриваются в последнюю очередь, после выбора шаблона. Подробнее про описанный алгоритм разрешения перегрузки можно почитать у Герба Саттера [Sutter2].

В C++11 появились шаблоны с переменным количеством параметров или вариативные шаблоны (variadic templates). Если для некоторого вызова допустимыми являются конкретизации вариативного шаблона и обычного, то последний всегда будет считаться более специализированным и, соответственно, выбран при разрешении перегрузки.


2.5.2. Принцип SFINAE

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

template
void Foo(const T* x);
// ...
Foo(42);

В этом случае, если в текущей области видимости есть перегруженные шаблоны, для которых аргументы выведены успешно, или перегруженные нешаблонные функции, то ошибки не возникает, такой шаблон просто «молча» исключается из разрешения перегрузки. Это и называется принципом SFINAE, который расшифровывается как Substitution Failure is not an Error (сбой при подстановке не является ошибкой).


2.5.3. Пример разрешения перегрузки

Рассмотрим пример перегруженных функций, шаблонов и полных специализаций шаблонов.

void Foo(int x); // нешаблонная функция
template
void Foo(T x); // шаблон 1
template<>
void Foo(double x); // полная специализация шаблона 1 для double
template<>
void Foo(const char* x); // полная специализация шаблона 1 для const char*
template
void Foo(const T* x); // шаблон 2, более специализированный чем шаблон 1
template
class U {/* ... */};
template
void Foo(U u); // шаблон 3, более специализированный чем шаблон 1

Посмотрим, как в соответствии с описанными выще правилами разрешается перегрузка для следующих вызовов:

Foo(42);       // #1 — нешаблонная функция
Foo(3.14);     // #2 — полная специализация шаблона 1 для double
Foo(42L);      // #3 — конкретизация шаблона 1 для long
Foo("meow");   // #4 — конкретизация шаблона 2 для char
Foo(U()); // #5 — конкретизация шаблона 3 для int

В первом вызове есть точно подходящая нешаблонная функция и одна точно подходящая конкретизация: шаблон 1 для int. Выбирается нешаблонная функция.

Во втором вызове нешаблонная функция подходит не точно, требуется преобразование от double к int. Есть одна точно подходящая конкретизация: шаблон 1 для double и есть полная специализация шаблона 1 для double, которая и выбирается.

В третьем вызове нешаблонная функция подходит не точно, требуется преобразование от long к int. Есть одна точно подходящая конкретизация: шаблон 1 для long, она и выбирается.

В четвертом вызове нешаблонная функция совсем не подходит, есть две точно подходящих конкретизации: шаблон 1 для const char* и шаблон 2 для char. Выбирается шаблон 2 как более специализированный и, соответственно, полная специализация шаблона 1 для const char* не рассматривается.

В пятом вызове нешаблонная функция совсем не подходит, есть две точно подходящих конкретизации: шаблон 1 для U и шаблон 3 для int. Выбирается шаблон 3 как более специализированный.


2.5.4. Управление перегрузкой шаблонов

Рассмотрим перегруженные функции и шаблоны:

void Foo(int x);     // нешаблонная функция
template // шаблон
void Foo(T x);

Нешаблонная функция будет выбрана только для аргументов типа int, int&, const int, const int&, для остальных целочисленных аргументов (long, short, unsigned int, etc.) будет выбрана шаблонная версия. В такой ситуации говорят, что шаблонная версия является жадной (greedy). Это не всегда является нужным поведением, например, часто желательно, чтобы первая функция выбиралась для всех целочисленных аргументов. Решить эту задачу можно несколькими способами. Можно добавить перегруженные функции для всех целочисленных типов, но это весьма утомительно. Другой вариант — использовать технику отключения шаблонов (template disabling). Для этого шаблон надо переписать в следующем виде:

template<
    typename T,
    typename S = std::enable_if_t::value>>
void Foo(T x);

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

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

template // целочисленные аргументы
void FooInt(T x);
template // остальные аргументы
void FooEx(T x);
// C++17
template
void Foo(T x)
{
    if constexpr (std::is_integral::value)
    {
        FooInt(x);
    }
    else
    {
        FooEx(x);
    }
}
// C++11
template
void Foo(T x)
{
    std::is_integral::value
        ? FooInt(x)
        : FooEx(x);
}

Описанные варианты требуют включения заголовочного файла .

2.6. Правила разрешения перегрузки для параметров «родственного» типа

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

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


  1. lvalue — именованные неконстантные переменные;
  2. константные (неизменяемые) lvalue — именованные константные переменные;
  3. rvalue — анонимные временные неконстантные переменные или lvalue, к которым применено преобразование std::move();
  4. константные (неизменяемые) rvalue — анонимные временные константные переменные.

Обе константные категории часто можно рассматривать как единую категорию — константы.

Рассмотрим теперь, допустимые категории аргументов для рассматриваемых типов параметров.
Пусть параметр имеет тип ссылки:

void Foo(T& x);

В этом случае допустимой категорией аргументов будет только lvalue.

Пусть параметр имеет тип rvalue-ссылки:

void Foo(T&& x);

В этом случае допустимой категорией аргументов будет только rvalue.

Пусть параметр имеет тип ссылки на константу или сам тип:

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

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

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

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

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

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

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

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

Здесь для констант и rvalue будет выбрана вторая функция, а вот для lvalue выбор будет неоднозначный.

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

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

Для любых аргументов выбор будет неоднозначный.

Для нестатических функций-членов квалификатор const позволяет перегружать функции в зависимости от константности скрытого параметра this.

class X
{
public:
    X();
    void Foo();       // this указывает на X*
    void Foo() const; // this указывает на const X*
    void DoSomething() const;
    void DoSomethingElse();
// ...
};
void X::DoSomething() const
{
// ...
    Foo(); // Foo() const
// ...
}
void X::DoSomethingElse()
{
// ...
    Foo(); // Foo()
// ...
}
// ...
X x;
x.Foo();  // Foo()
const X cx;
cx.Foo(); // Foo() const

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

class X
{
public:
    X();
    void Swap(X& other) noexcept;
// ...
};
// ...
X x;
// ...
X().Swap(x); // OK
x.Swap(X()); // ошибка, хотя и делает то же самое

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


2.6.2. Rvalue ссылки

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

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

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

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

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

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

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

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

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

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

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

Еще одно нововведение С++11, связанное с rvalue-ссылками — это ссылочные квалификаторы для нестатических функций-членов. Они позволяют перегружать по категории (lvalue/rvalue) скрытого параметра this.

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

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


2.6.3. Универсальные ссылки

В шаблоне

template
void Foo(T&& x);

тип параметра функции является не rvalue-ссылка, а так называемая универсальная ссылка (universal reference). Для таких шаблонов допустимы аргументы любой категории. Для lvalue аргументов тип x выводится как T&, для констант как const T&, для rvalue аргументов как T&&. Сам по себе параметр x является lvalue, поэтому если он используются в качестве аргумента какого-то внутреннего вызова к нему обычно применяется преобразование std::forward(), которое в случае, когда тип параметра T&&, превращает этот параметр в rvalue. Шаблоны с универсальными ссылками являются жадными (greedy), то есть при разрешении перегрузки у них высокий приоритет. Как бороться с жадными шаблонами, рассмотрено в разделе 2.5. Подробнее про универсальные ссылки и тесно связанную с ними прямую передачу (perfect forwarding) см. [Meyers2].


3.1. Параметры неполного типа

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

class X; // неполное объявление X
class Y; // неполное объявление Y
void Foo(X* x);
void Foo(Y* y);
// ...
X* px;
// ...
Foo(px); // void Foo(X* x);

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


3.2. Инициализация указателя на функцию

При инициализации указателя на функцию также можно использовать перегруженные функции.

void Foo(int x);
void Foo(const char* x);
// ...
void (*pF)(int) = Foo; // Foo(int)

Разрешение перегрузки работает

© Habrahabr.ru