Мы все привыкли к старому-доброму С++ вместе с его старыми-добрыми погрешностями и милыми недостатками. Мы с ними как-то сжились, научились игнорировать, а самые умные-даже избегать.
Но тяжелая поступь прогресса неостановима.
Прогресс принес нам современный С++ и вместе с ним новые, современные способы стрельбы себе по ногам - просто потому что стрелять по-ногам по-стариковски это фу.
В прошлый раз, как и полагается настоящему артисту, я выступил тут с выстраданным новаторским материалом, который вполне заслуженно вызвал некоторое количество эээ... скептических реакций.
По-одному из пунктов знающие люди (тм) прижали меня к стенке уничтожающим вопросом: а чего ж ты псишь на авто, мил человек?
Строго говоря я псил не на авто, а на использующих его без меры людей; а поскольку я сам по природе своей скептик, полагаю, что меры здесь быть не может: если есть возможность что-то использовать неправильно, то так оно и произойдет, а если еще и агитировать за это "что-то", то тогда вообще пиши пропало.
И вот в доказательство своей правоты я решил нырнуть немного глубже в тему и посмотреть, как еще можно неправильно приготовить (что непременно произойдет) этот чудесный подарок миру С++.
Для начала капельку теории.
Чем использование авто отличается от прописывания типов?
Попросту говоря, если вы пишете
int х = some_expression
то результат выражения, каким бы он ни был, будет приведен к указанному вами типу, если это вообще возможно; а если вы написали
auto х = ...
то тип х будет назначен компилятором исходя из типа выражения.
Разница маленькая, но потенциально значимая.
Хватит теории, даешь практику!
Рассмотрим такой простенький код.
enum X_0{
X0=0,X1,X2
};
enum X_1{
X20=3,X21,X22,X23
};
bool condition=false;
X_0 a=X0;
X_1 b=X20;
Скомпилируются ли следующие выражения?
auto c=condition?X0:X20;
auto d=!condition?X0:X23;
{
auto e=!condition?X22:-120;
auto f=condition?X0:-120;
auto g=condition?X0:6u;
auto h=condition?X0:0.2;
}
Hidden text
конечно, со свистом.
Будет пара ворнингов, но в целом все пучком.
А какие у них будут типы?
Не трудитесь - у всех, кроме 2 последних, тип результата будет один и тот же, int.
То есть здесь:
auto c=condition?X0:X23;
все нормально, максимум ворнинг огребете.
Но вот такое:
X_1 d=!condition?X0:X23;
уже no pasaran; такое выражение не скомпилируется, компилятор укажет вам на ошибку. Что очень хорошо, конечно, но есть, как принято сегодня говорить, "ньюанс”…
Оказывается, выбрать из 2 совершенно чуждых друг другу типов один, затолкать его в третий, совершенно левый, похерить исходные типы и сказать, что так и должно быть - эт пожалста, вообще никаких вопросов.
А сделать ровно то же самое, но воткнуть результат в один из исходных типов - да вы что себе позволяете?
П-последовательность.
Что же в этом случае дает нам авто, кроме поддержания программера в беззаботном состоянии?
Ничего хорошего.
Но это еще не все. Идем дальше.
Рассмотрим простую иерархию классов:
class A{
virtual int f(){return 0;};
};
class B:public A{
virtual int d(){return 0;};
};
class C:public B{
virtual int d1(){return 0;};
};
class D:public B{
virtual int d1(){return 0;};
};
Каков будет тип результата такого выражения?
{
auto vl=condition?new B:new C;
}
Здесь компилятор все правильно распедалит: тип результата будет конечно B*.
А тут:
{
auto vl=condition?new D:new C;
}
Думаете будет B*? Нет, будет затык, компилятор сломается.
Впрочем то же самое случится если мы запишем
{
B* vl=condition?new D:new C;
}
Хотя в таком виде
{
auto v2=condition?(B*)(new D):(B*)(new C);
B* v2=condition?(B*)(new D):(B*)(new C);
}
все сработает.
Что это означает?
оператор, который должен сам вычислить результирующий тип, отлично с этой задачей справляется, но лишь после того, как мы ему явно укажем, какой тип должен получиться!
Этому конечно есть умное и логичное объяснение, но все-таки ... в чем тогда выгода от авто в конкретном сценарии?
Ни в чем.
Но это еще не все, держитесь крепче.
глядите:
void rotate_one_by_one(std::array<int, 8> &arr)
{
auto temp = arr[arr.size() - 1];
auto i = arr.size() - 2; // Output : see the spoiler
for(; i > -1; --i)
{
arr[i+1] = arr[i];
}
arr.at(i+1) = temp;
}
void cyc_rotate(std::array<int, 8> &arr)
{
rotate_one_by_one(arr);
std::cout<<"After cyclic rotate\n";
for(auto n : arr)
std::cout<<n<<" ";
}
Output :
Hidden text
1 2 3 4 5 6 7 8
Но если мы строку
auto i = arr.size() - 2;
заменим на
long long i = arr.size() - 2
то результат будет уже другой:
Output :
Hidden text
8 1 2 3 4 5 6 7
Этому тоже есть простое и логичное объяснение, но может лучше такой ошибки не совершать?
А вот еще:
long y=1;
for (auto x = 100000001.0f; x <= 100000010.0f; ++x) {
++y;
}
Что тут произойдет, знаете?
Hidden text
Цикл никогда не закончится.
А в этом случае все будет хорошо:
for (double x = 100000001.0f; x <= 100000010.0f; ++x) {
++y;
}
Причины опять же очевидны и логичны, но лучше такую ошибку не совершать, да? Да.
Разумеется, в этом месте Опытный Молодой Специалист(тм) снисходительно улыбнется и скажет: дядя, ты что, совсем дурак? вот же у тебя буковка “f” в конце числа, удали ее и будет тебе счастье.
Оно-то да, но дело видите ли в том, что: авто предназначен самостоятельно выводить типы, вместо вас. Но если вы пишете "auto х=1.0f” это означает читерство - вы подсказываете авто, какой здесь должен быть тип. Парадокс? Нет. Так оно работает. Но ведь вы желаете использовать инструмент, который должен сделать за вас вашу работу и для этого вы делаете за него его работу, то есть делаете ровно то, что должны были сделать но не хотели, потому что рассчитывали на помощь инструмента!
Парадокс? Да, теперь это парадокс.
Есть и дополнительное возражение: а что если вместо этого числа поставлена символьная константа или переменная, и вы как-то не в курсе, какой у нее тип?
Куда "f" прибивать?
Ну и заезженный мной пример: Does "auto" keyword always evaluates floating point value as double?
Здесь от невнимательного использования авто происходит ошибка округления.
Так что, может на этом можно и закруглиться?
Нет, конечно, у меня еще есть сюрпризы.
Какой тип получит auto переменная в этом случае?
int x=0;
const int &y=x;
auto z=y;
Ответ:
int. Авто проигнорирует модификаторы и ссылки.
А в этом случае авто создаст копию объекта, вместо ссылки на него
for (const auto i : a)
{}
Чтоб иметь ссылку надо написать вот так
for (const auto& i : a)
{}
А здесь авто превратит массив в пойнтер:
int arr[10];
int *p = arr;
auto x = arr; // x is equivalent to *p
Ну может теперь уже хватит?
Нет! У меня есть еще!
auto data =134.29f;
auto v = data;
int dpcount = 0;
static double EPS = 1e-3;
while(v - (int)v > EPS){
v *= 10;
++dpcount;
std::cout<<"data:"<<data<<" v-(int)v "<< std::endl;
}
Обратите внимание на первую строку. Достаточно стереть букву “f” и цикл никогда не закончится.
Отдельная история с ламбдами.
Например:
auto A = [](){};
auto B = [](){};
Здесь переменные А и B имеют совершенно разные типы, которые задаются во время компиляции.
А если вы пользуетесь псевдоконтейнером std::vector<bool>, то тут уже правила еще более другие:
for (auto&& elem : container){}
Есть еще пара нюансов в использовании авто вместе с оператором "летающая тарелка", но это тема отдельной статьи.
В заключение добавим, что думают об использовании авто крутые спецы:
И как же это подытожить?
Да, авто работает. Старается, добросовестно вычисляет типы, честь ему и хвала. К сожалению весь остальной язык немного не рассчитан на такой уровень добросовестности. И вот на этом стыке возникают проблемы, которых тем больше, чем больше попыток авто заюзать.
Как будто в языке без этого недостаточно грабель.
Я охотно допускаю, что есть сценарии в которых без авто ну просто смерть.
Я на таковые до сих пор не натыкался, - слава богу, стараюсь новшества использовать по-минимуму - но многие говорят, что таки бывает острая нужда.
Ладно.
И еще некоторый недостаток в том, что auto называется именно так, перекрывая ключевое слово из богомерзкого С.
Вероятно, это проделки отчаянных борцов с легаси.
В конце концов что мешало назвать его, скажем, deduced или inferred? Вопрос, на который мы наверно никогда не узнаем ответа.
Пока-пока.