Ключевые слова auto и decltype – сходства, отличия и нюансы использования. Часть 1. auto как заместитель типа при инициализации.


Рассмотрим некоторый кусок кода :

std::vector<int> vect;
// ...
for(std::vector<int>::iterator it = vect.begin();it != vect.end();++it)
{
// делаем что-то с it;

Излишняя громоздкость объявления итератора it (которую можно уменьшить используя typedef или using) неудобна. При инициализации её можно избежать, поскольку тип переменной известен компилятору через тип инициализирующего значения и тут на помощь может прийти появившейся в С++11 auto.

std::vector<int> vect;
// ...
for(
auto it = vect.begin();it != vect.end();++it)
{
// делаем что-то с it;
}


Тип переменной it известен компилятору как возвращаемый из функции begin тип и соответствует std::vector<int>::iterator, так что тут всё выглядит очень просто. Но это только начало истории.

Рассмотрим пример применения auto:

int x = int(); // x типа int, инициализирован 0
assert(x == 0);
const int& crx = x; // crx —
константная ссылка на x
x = 42;
assert(crx == 42 && x == 42);

auto something = crx;// а какого типа something?

Вопрос с подвохом  касается типа something. Если тип crx - const int&, а crx является инициализирующим выражением для something, то ожидаемый наивным программистом тип const int&. Неправильно! В действительности something типа int. Продолжим пример:

assert(something == 42 && crx == 42 && x == 42);
// something
не const:
something = 43;
// something
не ссылка на x:
assert(something == 43 && crx == 42 && x == 42);


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

  2. Если, после выполнения п.1, имеется квалификатор верхнего уровня const и/или volatile, то он игнорируется.


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

Как вы возможно заметили, правила выглядят вполне аналогично правилам вывода типов шаблонных аргументов для функций исходя из типов аргументов функции. В действительности имеется небольшое отличие: auto может выводить тип std::initializer_list из инициализации с помощью фигурных скобок, в то время как вывод шаблонных аргументов функции — не может [2].

Продолжая предыдущий пример, предположим, что мы передаём ту же const int& crx в шаблонную функцию:

template<class T>
void foo(T arg);

foo(crx);


Итак, шаблонный аргумент Т выводится как int, а не как const int& , при инстанцировании foo аргумент arg имеет тип int, а не const int& (3 вариант у Мейерса- передача по значению). Если вы хотите, чтобы аргумент был const int& - вы можете достичь этого, специфицировав шаблонный аргумент при вызове:

foo<const int&>(crx);

или объявив функцию так:

template<class T>
void foo(const T& arg);

Последний вариант работает вполне аналогично с auto:

const auto& some_other_thing = crx;
Теперь some_other_thing имеет тип const int&, и это так для любого из следующих типов инициализирующего выражения - int, int&, const int, и const int&.
assert(some_other_thing == 42 && crx == 42 && x == 42);
//some_other_thing = 43;
ошибка компиляции — some_other_thing константна

x = 43;
assert(some_other_thing == 43 && crx == 43 && x == 43);
Теперь вспомним о двух отложенных ранее уточнениях к правилам вывода auto.


Первое уточнение
Рассмотрим пример
const int c = 0;
auto& rc = c;
rc = 44; //ошибка

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

Второе уточнение
Специальный случай, когда auto украшен rvalue ссылками
int i = 42;
auto&& ri_1 = i;
auto&& ri_2 = 42;

В обоих случаях тип инициализирующего выражения — int. Отсюда, в отсутствие специального правила, можно предположить что ri_1 и ri_2 имеют тип int. Неверно. Украшение auto rvalue ссылками требует от выведения типа работать по-другому:
  1. Если инициализирующее выражение lvalue, то украшенный && auto сначала выполняет выведение обычного типа, а затем добавляет к нему lvalue-ссылку,
  2. Если инициализирующее выражение rvalue, то украшенный && auto выполняет выведение обычного типа.
Чтобы понять каков каков результат этого, давайте посмотрим на пример ещё раз. Для случая 

ri_1 :
int i = 42;
auto&& ri_1 = i;

auto сначала выводит тип int. Затем видит, что i является lvalue. Значит, на втором шаге auto добавляет lvalue-ссылку, получая int&. Вместе с украшением &&, это даёт int& &&. По правилам склеивания ссылок в результате имеем int&.

Для случая ri_2 :
auto&& ri_2 = 42;
auto снова выводит тип int. Затем видит, что 42 является rvalue, и обработка прекращается. Вместе с украшением &&, получаем результат int&&.

Теперь, когда мы знаем как auto работает, давайте обсудим рациональные основания для этого. Имеется несколько путей аргументации. Например вот обоснование полезности демонтажа ссылочности и константности(с волатильностью). Быть ссылочностью — это не столько характеристика типа, сколько характеристика поведения переменной. Факт того, что инициализирующее выражение ведёт себя как ссылка не подразумевает, что инициализируемая переменная ведёт себя тоже как ссылка. Подобные соображения могут быть приложены и к константности(с волатильностью). Таким образом, auto не автоматически передаёт эти характеристики инициализирующего выражения в новую переменную. У меня есть опция придать эти характеристики моей новой переменной, используя синтаксис подобный const auto&, но это не происходит по умолчанию.

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

template<typename T, typename S>
void foo(T lhs, S rhs) {
auto prod = lhs * rhs;
//...
}

В стандартах ранее С++11 — это не могло быть сделано, поскольку тип произведения должен быть выведен каждый раз, когда шаблон функции инстанцируется. Например, если тип lhs и rhs - int, то и произведение будет int. Если один из множителей int, а второй double, то результат будет продвинут до double. С пользовательскими типами и их операторами умножения получаем бесконечное множество вариантов.

Строго говоря, auto в действительности никогда не необходим: в теории он может быть всегда заменен более мощным decltype. Например строка

auto prod = lhs * rhs;

может быть заменена на

decltype(lhs * rhs) prod = lhs * rhs;

что, конечно, гораздо менее элегантно. Кроме того, decltype выводит типы несколько иначе (что мы вскоре увидим), чем auto и использование decltype может повлечь использование дополнительного кода напр. метафункций типа remove_reference. Лучше не идти этим путем.

Источники:




Комментарии

Популярные сообщения из этого блога

Полезные новации С++17. Выражения свертки (Fold expressions)

Перегрузка и специализация шаблонных функций.

Ключевые слова auto и decltype – сходства, отличия и нюансы использования. Часть 3. decltype вывод типа сложного выражения.