Ключевые слова auto и decltype – сходства, отличия и нюансы использования. Часть 1. auto как заместитель типа при инициализации.
Рассмотрим
некоторый кусок кода :
std::vector<int>
vect;
// ...
for(std::vector<int>::iterator it = vect.begin();it != vect.end();++it)
{
// делаем что-то с it;
}
// ...
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;
}
// ...
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);
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);
// something не const:
something = 43;
// something не ссылка на x:
assert(something == 43 && crx == 42 && x == 42);
Здесь необходимо
привести правила, по которым выводится
тип из инициализирующего выражения.
Если инициализирующее выражение представляет собой ссылку — то ссылка игнорируется.
Если, после выполнения п.1, имеется квалификатор верхнего уровня const и/или volatile, то он игнорируется.
По поводу двух небольших уточнений к этим правилам,
проистекающим из украшения auto
квалификаторами и ссылками мы
поговорим немного позже.
Как вы возможно
заметили, правила выглядят вполне
аналогично правилам вывода типов
шаблонных аргументов для функций исходя
из типов аргументов функции. В действительности имеется небольшое
отличие: auto
может выводить тип std::initializer_list
из инициализации с помощью фигурных
скобок, в то время как вывод шаблонных
аргументов функции — не может [2].
Продолжая
предыдущий пример, предположим, что мы
передаём ту же
const int& crx
в шаблонную функцию:
template<class
T>
void foo(T arg);
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);
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 константна
assert(some_other_thing == 42 && crx == 42 && x == 42);
//some_other_thing = 43; ошибка компиляции — some_other_thing константна
| x = 43; |
Теперь вспомним о двух отложенных ранее уточнениях к правилам вывода auto.
Первое уточнение
Рассмотрим пример
const int c = 0;
auto& rc = c;
rc = 44; //ошибка
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 ссылками требует от выведения типа работать по-другому:
auto&& ri_1 = i;
auto&& ri_2 = 42;
В обоих случаях тип инициализирующего выражения — int. Отсюда, в отсутствие специального правила, можно предположить что ri_1 и ri_2 имеют тип int. Неверно. Украшение auto rvalue ссылками требует от выведения типа работать по-другому:
- Если инициализирующее выражение lvalue, то украшенный && auto сначала выполняет выведение обычного типа, а затем добавляет к нему lvalue-ссылку,
- Если инициализирующее выражение rvalue, то украшенный && auto выполняет выведение обычного типа.
Чтобы понять каков
каков результат этого, давайте посмотрим
на пример ещё раз. Для случая
ri_1
:
int i = 42;
auto&& ri_1 = i;
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;
//...
}
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.
Лучше
не идти этим путем.
Источники:
Комментарии
Отправить комментарий