Вступление
Доброго времени суток, друзья! Сегодня речь пойдёт об операторе 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)
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.ForserX Автор
22.09.2018 02:23Проблема в другом. Если под auto кроится поинтер, то всё в порядке. Но, к несчастью, этого не видно. А передавать ссылку на поинтер — это уже потеря тактов. Микроскопическая, конечно, но потеря.
Wilk
22.09.2018 02:25Во-первых, мне кажется, что компилятор достаточно умён для того, чтобы не допустить потери драгоценных тактов. Во-вторых, лично я не хочу усложнять код ради экономии на спичках тогда, когда эта экономия даже ничем не подтверждена.
ForserX Автор
22.09.2018 02:26Ты считаешь, что усложняет код явным написанием типа?
Wilk
22.09.2018 02:30Да. Потому что имя типа может быть длинным.
ForserX Автор
22.09.2018 02:31- Я уже писал, что для длинных типов я не против auto
- А using для его завезли?)
Wilk
22.09.2018 02:35using — это прекрасно, но если его надо написать для 5 типов, каждый из которых используется 1-2 раза в коде, я предпочту написать auto. Кроме того, код с auto намного проще преобразовать в обобщённый шаблонный код или перенести в обобщённую лямбда-функцию. Типы удалить, в принципе, не сложно, но это лишняя работа в таком случае.
mayorovp
22.09.2018 12:27Так ведь тут все просто же: голые указатели вообще не должны использоваться в современном коде. А значит, под it не может прятаться указатель!
Wilk
22.09.2018 13:00+1Здравствуйте!
Позвольте не согласиться с Вами: использование голых указателей вполне допустимо в современном коде. Нежелательным является ручное управление памятью — его принято отдавать на откуп RAII.
Примером вполне логичного и безопасного использования голых указателей является фильтрация потомков в иерархической структуре: родительский объект хранит владеющий умный указатель (std::unique_ptr или аналогичный) для каждого из дочерних объектов. В случае необходимости фильтрации и дальнейшей обработки дочерних объектов может быть сформирован список обычных указателей на объекты, которые необходимо обработать.
Если обобщить, то применение голых указателей оправдано тогда, когда каким-либо образом гарантируется валидность всех указателей на протяжении всего времени использования.
0xd34df00d
22.09.2018 17:30А как вы non-owning, reassignable-ссылки (ссылки в смысле PLT) обозначаете?
AntonSazonov
22.09.2018 13:53Извините, а о какой потери тактов вы говорите в случае ссылки на указатель? Можно поподробней?
Bronx
23.09.2018 06:26+1Если вы итерируетесь по коллекции голых указателей и хотите избежать двойной косвенности «ссылка на указатель на элемент» (и не хотите полагаться на оптимизацию), то лучше подчеркнуть это намерение явно, заменив
наfor (auto p : pointers)
.for (auto* p: pointers)
ForserX Автор
22.09.2018 02:35По поводу типов: смотря на какой компилятор нарвешься. Сейчас как в х32 обстоит всё — не знаю. Года два ничего не собирал на этом конфиге. Помню, что в VS 2010 кастовалось в float при тех же сложении и вычитании. Что в х64 у меня сыпало предупреждениями.
Wilk
22.09.2018 02:41Относительно типов я могу только судить по формальной спецификации того, как это должно быть. Как это на самом деле — другой вопрос, Вы в этом правы. Поэтому я последнее время предпочитаю использовать суффиксы для указания типов литералов. VS же вообще отдельная история, потому что каждый раз у ребят всё не по уму работает.
Sdima1357
22.09.2018 02:52Старый компилятор на 86 процессор кастил вообще к 80 бит float. В таком формате работал сопрцессор.
Serge78rus
22.09.2018 12:51Сопроцессор 8087 работал не только с long double
Sdima1357
22.09.2018 13:23Насколько я помню, все арифметические операции внутри — 80 бит. А загрузка и выгрузка может быть 32,64 или 80 бит
Serge78rus
22.09.2018 13:58В основном Вы правы, но есть так же комбинированные операции с одним операндом во внутреннем стеке и вторым во внешней памяти, они работают с разными форматами и, в зависимости от этого, требуют различного количества тактов на выполнение.
Sdima1357
22.09.2018 02:47auto isFloat =1.0f;// явный float32 но auto здесь неуместно
В случае с типами float32 float64 (double) нужно быть очень аккуратным из-за кастинга арифметикаи к старшему типу, поэтому типы костант лучше задавать явно
for (auto it : _bone_chains) //auto & it вернет Вам ссылку на элемент, а не копию
LimbUpdate(it);
/code>
. В случаях с натуральным числом 'e' нужно быть аккуратным. К примеру:
1e-8
Натуральное число "e" ( 2.7818 ...) тут не причем,
Wilk
22.09.2018 02:55[зануда-mode on]
Строго говоря, «е», основание натурального логарифма, является иррациональным числом.
[зануда-mode off]Sdima1357
22.09.2018 03:03Ну да, поэтому и точки :)
Но в данном выражении играет роль десятки. Напомнило старый анекдот, про преимущество define в С. «число пи вы можете задать defin-ом. Если его значение изменится, то вы легко сможете его поменять в одной строке»daiver19
22.09.2018 04:23Речь о том, что число е не натуральное.
П.С. А пример ничего не доказывает, кроме того, что нельзя сравнивать числа с плавающей точкой на точное равенство.
Serge78rus
22.09.2018 11:38но мне больше нравится полноценная концепция статической типизации
auto никоим образом не покушается на статическую типизацию языка. Статическая типизация и явная типизация — вовсе не синонимы.
WinPooh73
22.09.2018 18:33Только не оператор, а ключевое слово. В мире C++ эти термины существенно различаются по значению.
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;
Ни одного явно указанного типа
Chaos_Optima
24.09.2018 18:49А зачем они? Тут и так всё понятно. Знание типов редко когда нужно если код нормальный.
daiver19
… в большинстве случаев решается правильным именованием переменных. Ну и неясно, зачем вообще знать, что там за тип, если вам только «быстро посмотреть, что код делает». Реальный код же не должен быть нечитаемым мусором из примера. Реальный код был бы чем-то вроде 'auto crow = CrowFactory::GenerateCrow();' и тут тип вполне себе очевиден.
ForserX Автор
Я бы посоветовал ещё писать на вингерке, в таких случаях, но я не особо её фанат. А вообще, у Crow тут были реальные корни. И там был тип int.
daiver19
Заменять базовые типы на авто — это вообще нечто непонятное для меня. Как минимум int на одну букву короче :) В общем, говнокод — он и с auto говнокод, и без него.
ForserX Автор
Я и не такое видел. Один знакомый код на Луа кидал. На 5к строк всего одна переменная. Которая принимала в себя около 500 раз значения различных типов. Так что это ещё цветочки.