Секреты auto и decltype

от автора

Новый стандарт языка принят относительно давно и сейчас уже, наверное, нет программиста, который не слышал о новых ключевых словах 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_expressionT, 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_expressionT или const T, компилироваться это не будет, так как lvalue ссылку нельзя инициализировать rvalue. Если тип some_expressionT&, то и var будет иметь тип T&. Здесь важным моментом является то, что если тип some_expressionconst 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}); // Нет 

ссылка на оригинал статьи http://habrahabr.ru/post/206458/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *