Pull to refresh

Секреты auto и decltype

Reading time5 min
Views106K
Новый стандарт языка принят относительно давно и сейчас уже, наверное, нет программиста, который не слышал о новых ключевых словах 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}); // Нет
Tags:
Hubs:
Total votes 41: ↑41 and ↓0+41
Comments10

Articles