Введение
Все предыдущие примеры явно указывали тип каждой переменной — современный C++ (начиная с C++11) предоставляет auto для автоматического вывода типа переменной из её инициализатора, и decltype для получения типа выражения без его реального вычисления, и в этой статье разберём оба механизма вывода типов.
Концепция
auto при объявлении переменной с инициализатором заставляет компилятор вывести тип переменной из типа этого инициализатора — это не делает C++ динамически типизированным языком (тип всё равно строго фиксируется на этапе компиляции, просто не указывается явно разработчиком), но существенно упрощает синтаксис для случаев со сложными, длинными именами типов (особенно типичными для итераторов контейнеров или результатов шаблонных функций) и для случаев, где явное указание типа избыточно дублирует информацию, уже очевидную из самого инициализатора. decltype(expression) вычисляет ТИП заданного выражения без реального его выполнения — это особенно полезно в шаблонном коде, где тип возвращаемого значения функции может зависеть от типов её параметров шаблона неочевидным, требующим явного вычисления способом.
Пример кода
#include <iostream>
#include <vector>
#include <map>
int main()
{
auto x = 42; // тип выводится как int
auto y = 3.14; // тип выводится как double
auto name = std::string("Текст"); // тип выводится как std::string
std::vector<std::map<std::string, int>> complexData; // длинный, сложный тип
auto it = complexData.begin(); // БЕЗ auto пришлось бы писать ПОЛНЫЙ, длинный тип итератора явно
for (const auto &item : complexData) { // auto особенно ценен в циклах с итераторами/диапазонами
// ...
}
return 0;
}
// decltype — получение типа выражения без его реального выполнения
#include <iostream>
int getInt() { return 42; }
double getDouble() { return 3.14; }
decltype(getInt()) intResult = 10; // тип переменной — int, ВЫЧИСЛЕН из типа возвращаемого значения getInt(),
// САМА функция getInt() при этом РЕАЛЬНО НЕ ВЫЗЫВАЕТСЯ для этого вычисления
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) // тип результата зависит от типов T и U непредсказуемым заранее способом
{
return a + b;
}
int main()
{
auto result = add(3, 4.5); // result имеет тип double, ВЫВЕДЕННЫЙ через decltype(int + double) = double
std::cout << result << std::endl;
return 0;
}
// auto для возвращаемого типа функции (C++14) — упрощённый синтаксис без явного decltype
auto multiply(int a, int b) // тип возвращаемого значения ВЫВОДИТСЯ из тела функции
{
return a * b; // компилятор анализирует return-выражение и выводит тип int
}
Пояснения к коду
auto it = complexData.begin(); показывает классический, наиболее распространённый практический мотив использования auto — без него потребовалось бы написать полный, многословный тип итератора (std::vector<std::map<std::string, int>>::iterator), что и многословно, и хрупко при последующем изменении типа контейнера (потребовавшем бы изменения этого явного типа во множестве мест по всему коду), тогда как auto автоматически адаптируется к любому изменению типа complexData. add<T, U>(...) -> decltype(a + b) показывает классическое, до появления упрощённого auto для возвращаемого типа (C++14), использование decltype именно для случая, когда тип результата зависит от типов параметров шаблона нетривиальным образом (сложение int и double даёт double, но это неочевидно без явного вычисления через decltype именно выражения a + b, а не просто типа одного из параметров). Современный, упрощённый auto multiply(...) без явного decltype показывает, что для многих типичных случаев C++14 позволяет компилятору самостоятельно вывести тип возвращаемого значения непосредственно из анализа тела функции, без необходимости в более многословном, явном синтаксисе decltype.
Подводные камни
- Избыточное использование
autoтам, где явное указание типа значительно повышает читаемость кода — хотяautoсокращает многословность, для случаев, где тип переменной не очевиден из контекста инициализации (особенно при вызове функции с неочевидным из её имени типом возвращаемого значения), явное указание типа может быть значительно более информативным для читателя кода, и решение об использованииautoдолжно балансировать удобство написания с реальной читаемостью для будущих читателей кода, а не применяться механически, абсолютно везде, где это технически возможно. - Путаница относительно того, выводит ли
autoтип со ссылкой или без неё —autoпо умолчанию выводит тип БЕЗ ссылки (создавая копию), даже если инициализирующее выражение само является ссылкой, и для случаев, где реально нужна ссылка (избежание копирования), нужно явно написатьauto &илиconst auto &, и забытое явное указание ссылки там, где она реально нужна, приводит к неожиданному, неэффективному копированию там, где разработчик мог предполагать работу со ссылкой. - Применение
decltypeк выражению с побочными эффектами в ошибочном предположении, что выражение реально вычисляется — как явно отмечено в пояснении,decltype(getInt())НЕ вызываетgetInt()реально (это так называемый «unevaluated context», невычисляемый контекст), и попытка использоватьdecltypeдля выражения, чей побочный эффект разработчик ожидает увидеть, не даст этого эффекта, поскольку само выражение внутриdecltypeслужит исключительно для определения его типа, а не для его реального выполнения. - Зависимость от
autoдля типов, чьё конкретное представление критично для корректности кода (например, неявное изменение знаковости или размера целочисленного типа при изменении типа инициализирующего выражения в другом месте кода, статья 512) —autoавтоматически адаптируется к любому изменению типа инициализатора, что обычно удобно, но для случаев, где конкретный, точный тип переменной критичен для корректности последующих вычислений (избежание переполнения, корректная арифметика смешанных типов), неявная адаптацияautoк изменившемуся типу инициализатора где-то в другом месте кода может незаметно изменить и поведение кода, использующего эту переменную, без какого-либо явного предупреждения об этом изменении.