Новый стандарт языка принят относительно давно и сейчас уже, наверное, нет программиста, который не слышал о новых ключевых словах auto и decltype. Но как почти с любым аспектом С++, использование этих новых инструментов не обходится без нюансов. Некоторые из них я постараюсь осветить в этой статье.
Для разминки предлагаю начать с небольшого теста.
Тест
1. Какой тип будет у переменных ri1..riN после выполнения следующего кода?
int foo();
int& foo1();
const int foo2();
const int& foo3();
int main()
{
auto ri = foo();
auto ri1 = foo1();
auto ri2 = foo2();
auto ri3 = foo3();
auto& ri4 = foo();
auto& ri5 = foo1();
auto& ri6 = foo2();
auto& ri7 = foo3();
auto&& ri8 = foo();
auto&& ri9 = foo1();
auto&& ri10 = foo2();
auto&& ri11 = foo3();
int k = 5;
decltype(k)&& rk = k;
decltype(foo())&& ri12 = foo();
decltype(foo1())&& ri13 = foo1();
int i = 3;
decltype(i) ri14;
decltype((i)) ri15;
}
Скомпилируются ли следующие фрагменты?
2. auto lmbd = [](auto i){...};
3. void foo(auto i);
4. decltype(auto) var = some_expression; //WTF?!
5. auto var = {1, 2, 3}; //Если да, какой тип будет у var?
6. template<typename T> void foo(T t){}
foo({1, 2, 3});
Теория
К механизму вывода типов, используемому в шаблонах в С++11 добавилось два новых механизма: auto и decltype. И чтобы жизнь программистам не казалась медом, все эти 3 механизма выводят типы по-своему. Механизм, используемый auto, в точности копирует механизм шаблонов, за исключением типа std::initializer_list.
auto var = {1, 2, 3}; // Ok, var будет иметь тип std::initializer_list<int>
template<typename T> void foo(T t);
foo({1, 2, 3}); // Не компилируется
Объяснений такому поведению немного и все они не отличаются внятностью. Скотт Мейерс, например, по этому поводу пишет так: “I have no idea why type deduction for auto and for templates is not identical. If you know, please tell me!”. В С++14 этот механизм менять не собираются. За объяснение можно попровать принять тот факт, что работают, например, такие удивительные вещи:
template<typename T>
void fill_from_list(T& cont, const T& l);
std::vector<int> v;
fill_from_list(v, {1, 2, 3});
Auto
Итак, как же `auto` выводит тип? К сожалению, здесь нет простого правила на все случаи жизни, кроме, пожалуй, того, что `auto` при выводе типа в общем случае отбрасывает cv квалификаторы и ссылки. Ниже я перечислю самые важные моменты.
1.
auto var = some_expression;
Если тип some_expression T* или const T*, то тип var также будет T* или const T* соответственно. Пока без сюрпизов. Дальше — интереснее. Пожалуй самое важное с практической точки зрения правило заключается в том, что если тип some_expression — T, const T, T& или const T&, то типом var будет T. Это, впрочем, если задуматься, вполне логично, ведь в этом случае значение, возвращаемое some_expression копируется в var и можно смело писать вот так:
void foo(const std::list<widget_t>& l)
{
auto w = l.front();
l.pop();
// work with `w` here
}
2.
auto& var = some_expression;
В этом случае, ожидаемо, если тип some_expression — T или const T, компилироваться это не будет, так как lvalue ссылку нельзя инициализировать rvalue. Если тип some_expression — T&, то и var будет иметь тип T&. Здесь важным моментом является то, что если тип some_expression — const T&, то и тип var будет const T&.
3.
auto&& var = some_expression;
Здесь действует придуманное (или по крайней мере озвученное) Скоттом Мейерсом правило “универсальных ссылок”. Оно заключается в том, что тип var будет зависеть от того какая value category у some_expression. Если rvalue, то тип var будет T&&, если же lvalue, то T&. Cv квалификаторы при этом сохраняются.
Auto как параметр функции
auto нельзя использовать в качестве параметра функции и изменений в этом поведении не предвидется. Очевидно, тут дело в том, что если бы такое было разрешено, то, получается, любую обычную функцию можно было бы объявить по сути неявно шаблонной. И становится непонятно как разрешать перегрузку. Представьте себу такую ситуацию:
auto foo(auto v1, auto v2) -> decltype(v1+v2) ;
int foo(auto v1, bool v2);
foo(“C++ is cool?”, true);
Однако в с++14 можно будет использовать auto праметры в лямбдах.
decltype
С decltype ситуация с одной стороны сложнее (если посмотреть формальные правила), с другой стороны проще (если выделить основные моменты). Я сформулирую эти правила так, как я их понял.
Итак, следует различать два основных случая применения decltype.
1. decltype(var), когда var — это объявленная переменная (например в функции или как член класса). В этом случае decltype(var) будет иметь в точности тот тип, с которым объявлена переменная.
2. decltype(expr), expr — выражение. В этом случае типом decltype(expr) будет тип, которое могло бы вернуть это выражение, с той оговоркой, что decltype(expr) будет иметь тип T& (const T&), если expr возвращает lvalue, T, если expr возвращает rvalue типа Т (const T) и T&& (const T&&), если expr возвращает xvalue (rvalue reference).
Что значит “могло бы вернуть”? Это значит то, что decltype не вычисляет переданное ему в качестве аргумента выражение.
Несколько поясняющих примеров:
int i;
decltype(i); // int
decltype(i + 1); // int
decltype((i)); // int&
decltype(i = 4); //int&
const int foo();
decltype(foo()) ;// int
int&& foo1();
decltype(foo1()) ;// int&&
В том случае, если мы не знаем lvalue нам вернет выражение, rvalue или xvalue, а тип использовать хочется, можно воспользоваться стандартным шаблоном std::remove_reference, чтобы “очистить” тип от ссылок.
Decltype(auto)
Это новая “фишка” языка, которая войдет в С++14. Она нужна для сохранения семантики decltype при объявлении auto переменных и будет использоваться в тех случаях, когда нас не будет устраивать то, что auto отбрасывает ссылки и cv квалификаторы и, возможно, в связке с новой возможностью С++14 — выводом типа возвращаемого функцией значения.
const int&& foo();
auto i = foo(); // i будет иметь тип int
dectype(auto) i2 = foo(); // i2 будет иметь тип const int&&
В последнем случае мы могли бы написать decltype(foo()), но представьте, если бы вместо foo() было выражение на 2 строчки, а такие в С++ не редкость.
Ответы
Ну и сейчас, загрузив теорию в кэш, можно попытаться ответить на вопросы теста.
1.
int foo();
int& foo1();
const int foo2();
const int& foo3();
int main()
{
auto ri = foo(); // int
auto ri1 = foo1(); // int
auto ri2 = foo2(); // int
auto ri3 = foo3(); // int
auto& ri4 = foo(); // Не скомпилируется
auto& ri5 = foo1(); // int&
auto& ri6 = foo2(); // Не скомпилируется
auto& ri7 = foo3(); // const int&
auto&& ri8 = foo(); // int&&
auto&& ri9 = foo1(); // int&
auto&& ri10 = foo2(); // const int&&
auto&& ri11 = foo3(); // const int&
int k = 5;
decltype(k)&& rk = k; // Не скомпилируется
decltype(foo())&& ri12 = foo(); // int&&
decltype(foo1())&& ri13 = foo1(); // int&
int i = 3;
decltype(i) ri14; // int
decltype((i)) ri15; // int&
}
Скомпилируются ли следующие фрагменты?
2. auto lmbd = [](auto i){...}; // Сейчас - нет, но в С++14 - да
3. void foo(auto i); // Нет
4. decltype(auto) var = some_expression; // Да, в С++14
5. auto var = {1, 2, 3}; // Да, тип = std::initializer_list<int>
6. template<typename T> void foo(T t){}
foo({1, 2, 3}); // Нет
Автор: rpz