Как стать автором
Обновить

С++: стреляем по ногам по-современному

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров12K

Мы все привыкли к старому-доброму С++ вместе с его старыми-добрыми погрешностями и милыми недостатками. Мы с ними как-то сжились, научились игнорировать, а самые умные-даже избегать. 

Но тяжелая поступь прогресса неостановима.

Прогресс принес нам современный С++ и вместе с ним новые, современные способы стрельбы себе по ногам - просто потому что стрелять по-ногам по-стариковски это фу.

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

По-одному из пунктов знающие люди (тм) прижали меня к стенке уничтожающим вопросом: а чего ж ты псишь на авто, мил человек?

Строго говоря я псил не на авто, а на использующих его без меры людей; а поскольку я сам по природе своей скептик, полагаю, что меры здесь быть не может: если есть возможность что-то использовать неправильно, то так оно и произойдет, а если еще и агитировать за это "что-то", то тогда вообще пиши пропало.

И вот в доказательство своей правоты я решил нырнуть немного глубже в тему и посмотреть, как еще можно неправильно приготовить (что непременно произойдет) этот чудесный подарок миру С++.

Для начала капельку теории.

Чем использование авто отличается от прописывания типов?

Попросту говоря, если вы пишете

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){}

Есть еще пара нюансов в использовании авто вместе с оператором "летающая тарелка", но это тема отдельной статьи.

В заключение добавим, что думают об использовании авто крутые спецы:

The fundamental rule is: use type deduction only to make the code clearer or safer, and do not use it merely to avoid the inconvenience of writing an explicit type. When judging whether the code is clearer, keep in mind that your readers are not necessarily on your team, or familiar with your project, so types that you and your reviewer experience as unnecessary clutter will very often provide useful information to others.

И как же это подытожить?

Да, авто работает. Старается, добросовестно вычисляет типы, честь ему и хвала. К сожалению весь остальной язык немного не рассчитан на такой уровень добросовестности. И вот на этом стыке возникают проблемы, которых тем больше, чем больше попыток авто заюзать.

Как будто в языке без этого недостаточно грабель.

Я охотно допускаю, что есть сценарии в которых без авто ну просто смерть.

Я на таковые до сих пор не натыкался, - слава богу, стараюсь новшества использовать по-минимуму - но многие говорят, что таки бывает острая нужда.

Ладно.

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

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

В конце концов что мешало назвать его, скажем, deduced или inferred? Вопрос, на который мы наверно никогда не узнаем ответа.

Пока-пока.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы узнали что-то новое для себя из публикации?
31.62% Ничего нового, давно все знаю.37
20.51% Меньшая часть написанного оказалась сюрпризом.24
5.13% Большая часть написанного оказалась сюрпризом.6
18.8% Один сплошной сюрприз.22
23.93% Вообще не пользуюсь указанными в статье фичами, поэтому мне все равно.28
Проголосовали 117 пользователей. Воздержались 19 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какой версией С++ вы пользуетесь большую часть времени (т.е. используете характерные для этой версии фичи и НЕ используете фичи более старших версий)?
10.06% Что-то древнее, еще до 2011.17
18.93% 2011.32
28.99% 2017.49
14.2% 2020.24
27.81% Вообще не использую С++.47
Проголосовали 169 пользователей. Воздержались 20 пользователей.
Теги:
Хабы:
Всего голосов 29: ↑9 и ↓20-10
Комментарии76

Публикации

Истории

Работа

QT разработчик
9 вакансий
Программист C++
110 вакансий

Ближайшие события

12 – 13 июля
Геймтон DatsDefense
Онлайн
19 сентября
CDI Conf 2024
Москва