Секреты 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}); // Нет
    

    Комментарии 10

      +11
      auto lmbd = [](auto i){...}; // Сейчас - нет, но в С++14 - да

      Напомнило:
      In C++14 you just write «auto auto(auto auto) { auto; }». The compiler infers the rest from context.
        0
        Вобще, auto для простых типов хорошо подходит, но когда сложносоставные, то можно получить такую вот штуку
        auto smth = new Heavy_class<Some_class<Foo_bar>,int>();
        //....
        delete smth; // error: smth is not pointer
        
          +1
          А можно увидеть этот пример целиком? Больно похоже на ошибку компилятора.
          0
          > Очевидно, тут дело в том, что если бы такое было разрешено, то, получается, любую обычную функцию можно было бы объявить по сути неявно шаблонной. И становится непонятно как разрешать перегрузку.

          Очень даже понятно — именно так и разрешать, раскрыв auto в шаблонную функцию, а потом по обычным правилам. Т.е. ваш пример:
          auto foo(auto v1, auto v2) -> decltype(v1+v2) ; 
          int foo(auto v1, bool v2); 
          


          Раскрывается в:
          template<class V1, class V2>
          auto foo(V1 v1, V2 v2) -> decltype(v1+v2) ; 
          
          template<class V1, class V2>
          int foo(V1 v1, V2 v2); 
          


          Что, разумеется, приводит к неопределнности при вызове с любыми параметрами — по уже существующим правилам.

          Просто реальная область применения у подобного для функций небольшая. Вот для лямбд это было интересно, потому для них его и сделали.
            0
            Что, разумеется, приводит к неопределнности при вызове с любыми параметрами — по уже существующим правилам.

            Ваша версия кода — да, но, по-моему, «раскрытие» было выполнено неверно, должно быть:
            template<class V1, class V2>
            auto foo(V1 v1, V2 v2) -> decltype(v1+v2);
            
            template<class V1>
            int foo(V1 v1, bool v2);
            

            А это уже вполне себе компилируется.

            P.S. Это я не к тому, кто прав, а кто нет. Просто указать на неточность.
            +1

            Крайне неинтуитивные конструкции.

              +1
              Хотел бы рассказать еще один секрет про auto и decltype, но неохото создавать новую тему ибо она слишком мала.
              А секрет заключается в следующем:
              обращение к параметру this в статическом методе
              class Foo {
              	static auto self() -> decltype(this);
              };
                +1
                А это точно секрет, а не особенность реализации некоторых компиляторов? В стандарте оговорено что-нибудь об этом?
                  0
                  Но ведь…
                  using self = Foo;

                  … пишется быстрее, гораздо очевиднее и не содержит подводных камней.
                    +1
                    Во первых это был способ, а не цель.
                    Цель заключается в том, чтобы получить собственный тип не зная своего имени класса.
                    using self = Foo; — это конечно очевидно, но появляется возможность ошибки, например копипаста.
                    Можно сделать такой define:
                    #define DECLARE_SELF \
                    	static auto selfTypeHelper() -> UnPtr<decltype(this)>; \
                    	using Self = decltype(selfTypeHelper())

                    а потом применять его где это необходимо:
                    class Foo {
                    	DECLARE_SELF;
                    };

                    Ну а для чего нужен собственный тип? например для следующего:
                    ...
                    offsetof(Self, field);
                    ...

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое