Вступление


Доброго времени суток, друзья! Сегодня речь пойдёт об операторе auto в C++.
Небольшая предыстория: данный оператор пришёл к нам в C++11 и с тех пор глубоко вошёл в жизнь многих программистов и различных проектов. Сегодня я бы хотел рассказать о некоторых плюсах и минусах с моей точки зрения.


А с чем его едят?


Итак, где же его лучше всего, как мне кажется, применять?


1. Lambda


Вслед auto с 11 стандартом языка к нам пришли lambda functions. Рассмотрим на примере:


    // C++98
    struct 
    {
        bool operator()(int a, int b) const
        {   
            return a < b;
        }   
    } customLess;
    std::sort(s.begin(), s.end(), customLess);

Старый добрый std::sort с перегрузкой на свой алгоритм сортировки. В C++98 для такого кода требовалось создавать оболочку через структуру. С приходом лямбд ситуация сильно изменилась:


    // C++
    std::sort(s.begin(), s.end(), 
    // Lambda begin
    [](int a, int b) 
    {
        return a > b;   
    }
    // End
    );

2. STD Типы


Все мы когда-нибудь использовали std и используем сейчас. Но std типы имеют одну большую проблему… Они слишком длинные.


    int n1 = 3;

    std::vector<int> v{ 0, 1, 2, 3, 4 };
        // Случай 1
    std::vector<int>::iterator OldStyle = std::find(v.begin(), v.end(), n1);
        // Случай 2
    auto NewStyle = std::find(v.begin(), v.end(), n1);

Случай первый: std::find возвращает тип, равный std::vector<int>::iterator, Но мы это и сами прекрасно знаем. Во многих проектах, к примеру в том же игровом движке X-Ray, для таких итераций делались тайпдефы, аля:


typedef std::vector<int> vec_int;
typedef vec_int::iterator vec_int_it;

Но я не фанат такого кода, поэтому мне проще воспользоваться auto, как показано во втором случае.


3. Range-base-for loops


Долгожданное нововведение в C++ было R-B-For циклы. Ну, как нововведение, в C# и C++/CLI они уже были, а нативный всё так же отставал…
Когда нам нужно перебрать, допустим, элементы std::map, приходится обращаться к std::pait. Но, не стоит забыть про auto! Рассмотрим ситуацию, обратившись к исходному коду xray-oxygen:


        for (auto &entities_it : entities)
        {
            if (entities_it.second->ID_Parent != 0xffff) continue;

            found = true;
            Perform_destroy(entities_it.second);
            break;
        }

Хороший вид цикла. Без auto было бы так:


        for (std::pair<const u16, CSE_Abstract*> &entities_it : entities)
        {
            if (entities_it.second->ID_Parent != 0xffff) continue;

            found = true;
            Perform_destroy(entities_it.second);
            break;
        }

Как вы поняли, std::pair<const u16, CSE_Abstract*> будет отсылкой ко второму подзаголовку, ну да ладно, такие типы есть не только в STD Namespace, но и в самом X-Ray. Однако, в таких итераторах кроется ещё одна проблема, но об этом позже...


А чем же он плох?


1. Главная проблема — неясность типов...


Допустим, ситуация, имеем мы в коде:


auto WhyIsWhy = TestPointer->GetCrows();

Человеку, не писавшему этот код, нужно быстро глянуть содержимое функции, запускать IDE слишком долго, воспользуется Notepad++. Он натыкается на эту строку, и получает Logic Error начинается поиск объявления GetCrows функции.


2. Ссылка на объект массива или давайте поговорим ещё о Range-Base-For!


LimbUpdate(CIKLimb &refLimb);
...
{
       // Код из XRay Oxygen, система инверсной кинематики, не обращайте внимания, 
       // беру из того, что под рукой
    for (auto it : _bone_chains)
        LimbUpdate(it);
}

Вроде бы всё хорошо, но! Мы работаем не с элементом массива, а с копией объекта элемента. Из-за auto мы невидим, что it у нас не поинтер, а объект. Поэтому в местах, где у нас одномерный массив, советую писать типы полностью:


    for (CIKLimb &it : _bone_chains)
        LimbUpdate(it);

3. А у вас x64!


Те, кто работал в с написанием кода под AMD64, знают, что компилятор больше любит double, а не float. И наш друг auto кастуется во время компиляции в предпочитаемый программой тип. Рассмотрим на примере.


auto isFloat = 1.0; // 1.0f(x32) 1.0d(x64)

Конечно, такие вещи не критичны, но компилятор может начать сыпать кучу предупреждений по усечению типов, что не очень приятно, да и к тому же, постоянная запись в лог замедляет сборку. Но, есть ситуации, когда такой кастинг играет роль. В случаях с натуральным числом 'e' нужно быть аккуратным. К примеру:


auto eps = 1e-8; 

В x32 это значение равно: 9.999999939e-09
В x64 это значение равно: 1.0000000000000021e-08


Заключение


Конечно же, решать вам, где и как использовать auto, но мне больше нравится полноценная концепция статической типизации там, где она упрощает понимание кода.

Комментарии (33)


  1. daiver19
    22.09.2018 02:19

    Главная проблема — неясность типов.

    … в большинстве случаев решается правильным именованием переменных. Ну и неясно, зачем вообще знать, что там за тип, если вам только «быстро посмотреть, что код делает». Реальный код же не должен быть нечитаемым мусором из примера. Реальный код был бы чем-то вроде 'auto crow = CrowFactory::GenerateCrow();' и тут тип вполне себе очевиден.


    1. ForserX Автор
      22.09.2018 02:25

      Я бы посоветовал ещё писать на вингерке, в таких случаях, но я не особо её фанат. А вообще, у Crow тут были реальные корни. И там был тип int.


      1. daiver19
        22.09.2018 02:26

        Заменять базовые типы на авто — это вообще нечто непонятное для меня. Как минимум int на одну букву короче :) В общем, говнокод — он и с auto говнокод, и без него.


        1. ForserX Автор
          22.09.2018 02:29

          Я и не такое видел. Один знакомый код на Луа кидал. На 5к строк всего одна переменная. Которая принимала в себя около 500 раз значения различных типов. Так что это ещё цветочки.


  1. Wilk
    22.09.2018 02:19

    Здравствуйте!

    Мне кажется, что как минимум проблема с применением auto в range-based for надумана: ничто не мешает писать так:

    LimbUpdate(CIKLimb &refLimb);
    ...
    {
           // Код из XRay Oxygen, система инверсной кинематики, не обращайте внимания, 
           // беру из того, что под рукой
        for (auto& it : _bone_chains)
            LimbUpdate(it);
    }
    


    Относительно же чисел с плавающей точкой я не уверен: мне казалось, что если тип не указан явно (так или иначе), всегда будет использован double. Довольно часто можно даже увидеть предупреждение на тему преобразования из double во что-то с потерей данных. Исключениями могут быть результаты вычисления выражений, если приведение типов требует типа, отличного от double, например, если все операнды типа float.


    1. ForserX Автор
      22.09.2018 02:23

      Проблема в другом. Если под auto кроится поинтер, то всё в порядке. Но, к несчастью, этого не видно. А передавать ссылку на поинтер — это уже потеря тактов. Микроскопическая, конечно, но потеря.


      1. Wilk
        22.09.2018 02:25

        Во-первых, мне кажется, что компилятор достаточно умён для того, чтобы не допустить потери драгоценных тактов. Во-вторых, лично я не хочу усложнять код ради экономии на спичках тогда, когда эта экономия даже ничем не подтверждена.


        1. ForserX Автор
          22.09.2018 02:26

          Ты считаешь, что усложняет код явным написанием типа?


          1. Wilk
            22.09.2018 02:30

            Да. Потому что имя типа может быть длинным.


            1. ForserX Автор
              22.09.2018 02:31

              1. Я уже писал, что для длинных типов я не против auto
              2. А using для его завезли?)


              1. Wilk
                22.09.2018 02:35

                using — это прекрасно, но если его надо написать для 5 типов, каждый из которых используется 1-2 раза в коде, я предпочту написать auto. Кроме того, код с auto намного проще преобразовать в обобщённый шаблонный код или перенести в обобщённую лямбда-функцию. Типы удалить, в принципе, не сложно, но это лишняя работа в таком случае.


      1. mayorovp
        22.09.2018 12:27

        Так ведь тут все просто же: голые указатели вообще не должны использоваться в современном коде. А значит, под it не может прятаться указатель!


        1. Wilk
          22.09.2018 13:00
          +1

          Здравствуйте!

          Позвольте не согласиться с Вами: использование голых указателей вполне допустимо в современном коде. Нежелательным является ручное управление памятью — его принято отдавать на откуп RAII.

          Примером вполне логичного и безопасного использования голых указателей является фильтрация потомков в иерархической структуре: родительский объект хранит владеющий умный указатель (std::unique_ptr или аналогичный) для каждого из дочерних объектов. В случае необходимости фильтрации и дальнейшей обработки дочерних объектов может быть сформирован список обычных указателей на объекты, которые необходимо обработать.

          Если обобщить, то применение голых указателей оправдано тогда, когда каким-либо образом гарантируется валидность всех указателей на протяжении всего времени использования.


        1. 0xd34df00d
          22.09.2018 17:30

          А как вы non-owning, reassignable-ссылки (ссылки в смысле PLT) обозначаете?


      1. AntonSazonov
        22.09.2018 13:53

        Извините, а о какой потери тактов вы говорите в случае ссылки на указатель? Можно поподробней?


      1. Bronx
        23.09.2018 06:26
        +1

        Если вы итерируетесь по коллекции голых указателей и хотите избежать двойной косвенности «ссылка на указатель на элемент» (и не хотите полагаться на оптимизацию), то лучше подчеркнуть это намерение явно, заменив

        for (auto p : pointers) 
        на
        for (auto* p: pointers)
        .


    1. ForserX Автор
      22.09.2018 02:35

      По поводу типов: смотря на какой компилятор нарвешься. Сейчас как в х32 обстоит всё — не знаю. Года два ничего не собирал на этом конфиге. Помню, что в VS 2010 кастовалось в float при тех же сложении и вычитании. Что в х64 у меня сыпало предупреждениями.


      1. Wilk
        22.09.2018 02:41

        Относительно типов я могу только судить по формальной спецификации того, как это должно быть. Как это на самом деле — другой вопрос, Вы в этом правы. Поэтому я последнее время предпочитаю использовать суффиксы для указания типов литералов. VS же вообще отдельная история, потому что каждый раз у ребят всё не по уму работает.


        1. Sdima1357
          22.09.2018 02:52

          Старый компилятор на 86 процессор кастил вообще к 80 бит float. В таком формате работал сопрцессор.


          1. Serge78rus
            22.09.2018 12:51

            Сопроцессор 8087 работал не только с long double


            1. Sdima1357
              22.09.2018 13:23

              Насколько я помню, все арифметические операции внутри — 80 бит. А загрузка и выгрузка может быть 32,64 или 80 бит


              1. Serge78rus
                22.09.2018 13:58

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


  1. Sdima1357
    22.09.2018 02:47

    auto isFloat =1.0f;// явный float32 но auto здесь неуместно

    В случае с типами float32 float64 (double) нужно быть очень аккуратным из-за кастинга арифметикаи к старшему типу, поэтому типы костант лучше задавать явно
    for (auto it : _bone_chains) //auto & it вернет Вам ссылку на элемент, а не копию
    LimbUpdate(it);
    /code>

    . В случаях с натуральным числом 'e' нужно быть аккуратным. К примеру:


    1e-8
    Натуральное число "e" ( 2.7818 ...) тут не причем,


    1. Wilk
      22.09.2018 02:55

      [зануда-mode on]
      Строго говоря, «е», основание натурального логарифма, является иррациональным числом.
      [зануда-mode off]


      1. Sdima1357
        22.09.2018 03:03

        Ну да, поэтому и точки :)
        Но в данном выражении играет роль десятки. Напомнило старый анекдот, про преимущество define в С. «число пи вы можете задать defin-ом. Если его значение изменится, то вы легко сможете его поменять в одной строке»


        1. daiver19
          22.09.2018 04:23

          Речь о том, что число е не натуральное.

          П.С. А пример ничего не доказывает, кроме того, что нельзя сравнивать числа с плавающей точкой на точное равенство.


  1. Serge78rus
    22.09.2018 11:38

    но мне больше нравится полноценная концепция статической типизации
    auto никоим образом не покушается на статическую типизацию языка. Статическая типизация и явная типизация — вовсе не синонимы.


  1. WinPooh73
    22.09.2018 18:33

    Только не оператор, а ключевое слово. В мире C++ эти термины существенно различаются по значению.


  1. Tuxman
    23.09.2018 08:17

    std::pait std::pair


    1. berez
      23.09.2018 15:00
      +1

      Вот еще хорошо получилось:

      Из-за auto мы невидим,



  1. Jeka178RUS
    24.09.2018 14:07

    Я принципиально избегаю auto. Лучше сделать человеческий typedef что бы все было красиво и понятно.

    В этой статье было еще несколько примеров использования auto. Причем если злоупотребить получиться совсем страшный пример:

    #include <tuple>
     
    auto get() {
    	return std::make_tuple("fred", 42);
    }
    
    auto [name, age] = get();
    std::cout << name << " is " << age << std::endl;


    Ни одного явно указанного типа


    1. Chaos_Optima
      24.09.2018 18:49

      А зачем они? Тут и так всё понятно. Знание типов редко когда нужно если код нормальный.