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

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

Мы сами не ожидали, что такое может быть.

Отличный пример того, как не надо относиться к неуточнённому/неопределённому поведению.

Были ли люди, которые ответили, что может вывести и 1, 1? Спорили ли вы с ними, что не может? (:
Даже без знания плюсов, но со знанием лени, в голову сразу приходит вариант с 1, 1.
Еще очевиднее потому -что " Порядок вычисления аргументов не определён. Все аргументы должны быть вычислены до выполнения тела вызываемой функции"
Или 1,1 или 2,2. VS компилятор самый логичный.
Мне вообще «1, 1» кажеится самым очевидным: если мы должны два разwiа увеличить переменную на 1, то дешевле же объединить их в обно увеличиение.

И да, в C++17 это убрали, так как смысла в этом нету: любая программа, которую можно таким образом «ускорить» — некорректна… а смысл ускорять некорректную программу?
И да, в C++17 это убрали, так как смысла в этом нету: любая программа, которую можно таким образом «ускорить» — некорректна… а смысл ускорять некорректную программу?

только внезапно эти некорректные программы стали корректными. Черт. Не хочу с этим языком иметь ничего общего — это ж надо догадаться наплодить столько деталей, что ни один вменяемый человек в голове их не удержит (что и доказывает эта статья, т.к. обвинить авторов анализатора в непрофессионализме попросту невозможно)

НЛО прилетело и опубликовало эту надпись здесь
И забавно, что когда даже делают лучше (например, теперь memcpy и прочие начинают лайфтаймы некоторых видов объектов), по факту делают и хуже (так как теперь надо помнить, каких именно объектов, и начиная с какой версии C++, и старое поведение забывать нельзя).
А есть в каких-то компиляторах закладка на это?

По моему как раз тут успели вовремя: разработчики компиляторов эту недоговорку не успели истолковать в свою пользу, так что можно считать что во всех стандартах всё как в последнем.

Меня больше радуют фанаты подхода «если компилятор ругается — то всё плохо». Ну вот, например… компилятор ругается — и где ж там, собственно, проблема?
Ну вот, например… компилятор ругается — и где ж там, собственно, проблема?

Причем некоторым статическим анализаторам, например, если не ошибаюсь, SonarQube, не понравилось бы, если бы инициализация i выполнялась в user-provided default constructor — он бы завопил, что его нужно выкинуть, а инициализацию производить в in-class initializer.
НЛО прилетело и опубликовало эту надпись здесь
То есть, вы предлагаете помнить не только стандарт, но и то, как какие компиляторы себя ведут?
В случаях, когда стандарт что-то, что ранее было нелегальным сделал легальным (и почему-либо нельзя перейти на последнюю версию стандарта) — это разумно.

спасибо, мы не знали, что тут просто скастовать небезопасно, и надо приседать с memcpy и placement new, прикольно получилось, live and learn
Ну про эту же историю написано всё в описании std::bit_cast.

Но да, что со всем этим делать — непонятно. UBSAN ловит далеко не всё…
НЛО прилетело и опубликовало эту надпись здесь
Их и не надо в голове держать.
Нормально пиши — нормально будет.

Так для того, чтобы "нормально писать", эти детали держать в голове и надо.

Нет. С++ многоуровневый, если оставаться в рамках возможностей уровня который хорошо знаешь — не возникнет проблем.
Собственно даже пример из статьи — зачем так писать? Мне не нужно знать UB там или какое-то конкретное поведение. Я просто не будут так писать.
Либо ты знаешь как работает конструкция и используешь её, либо не знаешь и не используешь. Всё очень просто.
Проблемы здесь только у тех, кто бездумно копипастит код со стэоверфлоу и из других источников.
Либо ты знаешь как работает конструкция и используешь её, либо не знаешь и не используешь.

Не хватает третьего варианта: думаешь, что знаешь, как работает.

Нет такого варианта.
«Не думаешь что знаешь», а «Догадываешься»
С++ не рокет сайнс и в рамках стандарта он предсказуем, один раз прочитал ииспользуешь и знаешь как работает.
Еще раз: никто не заставляет лезть в сложные конструкции. Не уверен — не используй.

Это вообще бич современных программистов. Я не так давно с этим столкнулся. Мне повезло, в течении 15 лет я работал со специалистами как минимум не ниже меня по уровню. Очень привык. А тут довелось поработать в команде с джунами, которые при этом дозволено без ревью комитить. Использование «догадок» постоянно. Вместо того чтобы разобраться как работает — делается по принципу «попробую все варианты, какой заработает, тот и оставлю». И вот этого С++ не прощает. Потому что догадки не имеют отношения к реальности. Ты либо знаешь и используешь, либо не знаешь и не используешь. Третий варианто только один — идиот, которые использует не зная, а «догадываясь».
Может хватит в угадайку играть?
Простите, наболело.
С++ не рокет сайнс и в рамках стандарта он предсказуем, один раз прочитал ииспользуешь и знаешь как работает.

ага, только вот обилие стандартов и их имплементаций… Впечатляет. Поэтому, наверное, нужно договориться заранее — какой используете

Не надо договариваться. Если каждый будет писать код понимая что он пишет — этого будет достаточно.
Есть очень простое правило: в написанном тобой коде ты должен понимать что делает КАЖДАЯ строчка, а вернее даже каждый символ написанный тобой. Зачем ты его написал и как он себя ведет.
Не надо бездумно копипастить чужой код и будет счастье.
Да, если человек не способен следовать этому простому правилу — не надо лезть в IT-вообще.
Есть очень простое правило: в написанном тобой коде ты должен понимать что делает КАЖДАЯ строчка, а вернее даже каждый символ написанный тобой. Зачем ты его написал и как он себя ведет.
Это прошлый век, сейчас так никто не пишет. В больших проектах это зачастую просто невозможно — так как вы используете компоненты, о точном предназначении которых можно только догадываться.

Ещё знать язык, которые вы используете (даже такой сложный, как C++) — можно и нужно, все библиотеки (включая сюда и предоставляемые OS сервисы)… невозможно в принципе.

Не надо бездумно копипастить чужой код и будет счастье.
А как быть, если в описании библиотеки есть только пример и нет подробного описания как составляющие этот пример 10-15 строк взаимодействуют между собой?

А сегодня это если ещё не единственный вариант — то типичный.
Я не говорю о том, чтобы знать как внутри работает библиотечный метод.
Но как минимум у вас должен быть ответ почему вы вызываете этот метод.
Но как минимум у вас должен быть ответ почему вы вызываете этот метод.

Этого, к сожалению, недостаточно. Т.к. библиотечный метод может принимать довольно странные комбинации аргументов, давать достаточно неожиданные возвратные значения. Да еще и исключения кидать. И все это нужно держать в голове

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

Надо определиться.
О какой библиотеке речь?
О стандартной? Или о какой-то third-party, написанной джунами?

Любую. Мы же не в абстрактно мире живем. А продакшн диктует свои правила. И либа без документации и сорсов — вполне себе часть жизни специалиста. Кто-то может выбирать(я, например, работаю с OS решениями, так проблемы низкой документированности нет в принципе — лезешь в сорсы и разбираешься), кто-то не может. В любом случае очень часто приходится работать с чужим API как с черным ящиком. Есть только вход и выход, а что внутри — тайна.
один раз прочитал и используешь и знаешь как работает

Все несколько сотен страниц?

НЕ имеет значения. Вот сколько прочитал, столько и используешь.
Если не хватает — читаешь ту часть, которую надо и продолжаешь спокойно жить с этим знанием.
НЛО прилетело и опубликовало эту надпись здесь
Но зачем, Карл?
Можно абсолютно спокойно жить прочитав С++ для чайников. Главное не писать код, который не понимаешь и всё.
Для меня самым очевидным является оптимизация и подстановка констант, так как переменные уже нигде не используются. Некоторые компиляторы так и делают

ссылка, где можно выбрать компилятор и его опции и посмотреть ассемблер
        push    1
        push    1
        push    OFFSET `string'
        call    _printf


И даже больше — некоторые убирают вызов F1 и делают inline кода прямо в main`e.

Да еще вспоминаем, чем отличается i++ от ++i и кажется все логично — результат 1 1.

Было бы интересно посмотреть на вопросы шарпистам. Не хотите "спалить" какой-нибудь красивый вопрос в следующей статьей? ))

Мы подумаем :).
Если б ещё вопрос в этой статье был красивым… А то UB как UB. Стандартный ответ «не знаю, и предполагать не буду».
Стандартный ответ «не знаю, и предполагать не буду

Так у них же другая специфика по сравнению с другими фирмами использующими C++.
У них С++ это не только язык программирования, но и предметная область.

У них С++ это не только язык программирования, но и предметная область

И что? Для диагностики вполне достаточно написать «вот здесь неопределённое поведение». Не требуется никакой приписки «и мы предполагаем, что поведёт оно себя вот так».
Не требуется никакой приписки «и мы предполагаем, что поведёт оно себя вот так»

Это конечно из раздела "мое воображение", но оно может потребоваться при общении с клиентом, который скажет ваш инструмент пишет что здесь у меня "UB", а у меня все работает, в таком случае знание что "если собрать компилятором X с опциями Y, то UB покажет себя во всей красе" может сэкономить кучу времени на объяснения что такое UB и почему это плохо.


Из личной практики, как-то на ревью кода я пытался убедить что стоит все-таки писать va_end иначе это противоречит стандарту, а мне в ответ тыкали тем, что вот с данным компилятором и на данной платформе va_end это пустой макрос.


Но вообще скорее всего это вопрос в том числе и способ "повернуть" разговор на
тему как работает компилятор и какие у современных компиляторов есть оптимизации
использующие undefined/unspecified behaviour. Ведь эти грабли с неопределенным
порядком вычисления аргументов положили не просто для того чтобы сделать жизнь
пользователей C++ поинтереснее.

Это конечно из раздела «мое воображение», но оно может потребоваться при общении с клиентом, который скажет ваш инструмент пишет что здесь у меня «UB», а у меня все работает

Да, фактор дебилов я не учёл. Но не понимаю, почему вопросы с дебилами должны решать программисты.
Но не понимаю, почему вопросы с дебилами должны решать программисты.

Ну опять же специфика проекта (опять же воображение, так как я никак не связан с PVS): в теории можно иметь и в отделе поддержки людей хорошо знающих и разбирающихся в C++, но это немного дорого по-моему делегировать таким людям только поддержку, рационально на мой взгляд в случае такой предметной области распределить поддержку пользователей среди программистов-добровольцев.

Да, поддержка, это большая, важная, существенная и ответственная часть жизни нашей команды. Кстати, думаю многим будет интересен вот такой доклад :).
Юрий Минаев: Не связывайтесь с поддержкой C++ программистов.
  • «Не знаю», это совсем не ответ.
  • А вот UB — интереснее. Это можно пообсуждать? Почему UB? Где тут UB?
Ну почему же, вполне себе ответ. Я не знаю многих мест с++, где встречается UB (ну кроме популярных, как в этом примере), но на подозрительные места глаз немного натренировался. И обычно мой ответ на такие вопросы «не знаю, но код лучше бы переписать так, чтобы не требовать от читающего знания таких тонкостей».

Но у вас, как уже заметили, своя специфика.

Да, у меня такие же рассуждения, примерно.

реквестирую от вас статью ТОП-10 вопросов на наших собеседованиях.


кстати, а у вас есть проверки на такой код:
memset(static_cast<void*>(&settings), 0, sizeof settings);
?

memset(static_cast<void*>(&settings), 0, sizeof settings);
Мало данный и непонятно что хочется найти. Что такое `settings`? Что вокруг `memset` в коде?

Да, данных мало. Объект создаётся, зануляется, заполняется и потом передаётся аргументов в функцию.


Меня зацепил просто тот факт, что в случае, когда всё хорошо со структурой, то данное явное приведение избыточно: memset() принимает void*, а любой указатель может к нему приводиться неявно.


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


ЕМНИП, memset и memcpy на нетривиальные типы — это неопределённое поведение. Конкретно здесь, всё было и будет хорошо с точки зрения поведения с используемым компилятором (хотя я не могу себе представить, как тут можно напортачить или испортить жизнь разработчику и потребителю кода): нет виртуальных функций, прямым доступом инвариант не поломаешь и так далее.


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


И в дополнение, сама структура из сторонней библиотеки, конструктор там появляется под условной компиляцией, если сборка C++ компилятором. Примеры для библиотеки идут, в основном, для Си и там повсеместно memset.


В общем, наличие явного приведения типа в memset/memcpy, хоть и не является ошибкой, но очень подозрительно.


ЗЫ Я предложил использовать {} для инициализации, типа:


Foo settings{};

Хотя да, я встречал реализации memset/memcpy на контроллерах, что принимали uint8_t*, но мне кажется, это уже другой пласт проблем.

struct SettingsA
{
  int x;
  SettingsA() : x(10) {}
};

struct SettingsB
{
  int x;
  SettingsB() : x(10) {}
  virtual void V() {}
};

void Set()
{
  SettingsA settings_a;
  memset(static_cast<void*>(&settings_a), 0, sizeof settings_a); // тихо

  SettingsB settings_b;
  memset(static_cast<void*>(&settings_b), 0, sizeof settings_b); // V598
}


У PVS-Studio здесь вечный компромисс между дотошностью и шумностью. Он промолчит про случай SettingsA, хотя формально так делать нельзя. Почему? Потому, что в мире слишком много такого кода. И на практике он нормально работает. Я не говорю, что так нужно/можно писать. Однако, не хочется воевать с ветряными мельницами.

Если дело серьезней, например, есть указатель на таблицу виртуальных методов, то тут уже молчать никак нельзя. Вызов memset явно всё портит и анализатор выдаст:
V598 The 'memset' function is used to nullify the fields of 'SettingsB' class. Virtual table pointer will be damaged by this. test.cpp 39

P.S. И да, есть отважные программисты: Примеры ошибок, обнаруженных с помощью диагностики V598.

Да, я написал, что по факту ничего не ломается. Но интересна мотивация написания лишних букв. Хотя хорошо, что PVS разделяет два случая, по крайней мере, если уж написано, поможет не отстрелить ногу. Но сколько мест, где нет ещё статического анализа, а тут и компилятор помочь предупреждением может, а его затыкают.

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

Дальше оказалось, что это маскировка предупреждения компилятора о том, что структура нетривиальная. И действительно, у неё оказался конструктор, в котором происходит установка значения полей.
Что совершенно не запрещает использовать memset и не вызывает предупреждений у clang/gcc/icc/msvc.

Возможно в вашем примере было что-то ещё?

Потому что желание использовать memset как раз понятно. Он эффективнее. Сравните bar и baz.

Хотя это «экономия на спичках», конечно.
НЛО прилетело и опубликовало эту надпись здесь
Всё так. Но учтите, что gcc до версии 7 так не умеет, а clang, наоборот, аж с версии 3.2 умеет и без memset и станет понятно, что «стреляет» эта оптимизация не так, чтобы уж очень часто.

Но, с другой стороны, если memset никогда не хуже и иногда лучше — то почему бы и нет?
НЛО прилетело и опубликовало эту надпись здесь
не вызывает предупреждений у clang/gcc/icc/msvc

я же не на ровном месте написал, что вызывает предупреждение компилятора:


#include <iostream>
#include <cstdlib>
#include <cstring>
#include <type_traits>

using namespace std;

struct foo_t
{
    int a;
    foo_t() 
    {
        a = 0;
    }
};

int main()
{
    foo_t settings;
    memset(&settings, 0, sizeof(settings));
    cout << std::is_trivial_v<foo_t> << '\n';
}

https://wandbox.org/permlink/1O3QXcXX4C3ZSOQt


Предупреждение:


prog.cc: In function 'int main()':
prog.cc:21:42: warning: 'void* memset(void*, int, size_t)' clearing an object of non-trivial type 'struct foo_t'; use assignment or value-initialization instead [-Wclass-memaccess]
   21 |     memset(&settings, 0, sizeof(settings));
      |                                          ^
prog.cc:9:8: note: 'struct foo_t' declared here
    9 | struct foo_t
      |        ^~~~~

Потому что желание использовать memset как раз понятно. Он эффективнее.

Пока у структуры не появится инвариант, который можно поломать.

НЛО прилетело и опубликовало эту надпись здесь

Я потому тему и поднял, потому как упоминание UB я встречал в https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-memset но там конкретно:


Using memcpy to copy a non-trivially copyable type has undefined behavior.

т.е. только про memcpy(), хотя, положа руку на сердце, все аргументы, которые у меня в голове рождаются касательно memcpy(), вполне себе применимы и для memset().


И да, там речь идёт о non-trivialy-copyable больше.

И таки в стандарте я пока не откопал нужной строчки, пока где-то рядом, да около. Сам GCC:



For example, the call to memset below is undefined because it modifies a non-trivial class object and is, therefore, diagnosed.

И пример кода:


std::string str = "abc";
memset (&str, 0, sizeof str);

Я пока ничего не утверждаю и просто пытаюсь для себя разобраться. В частности, почему при копировании может быть UB, а при memset — нет и всё штатно (ну пока оно без vtable, хотя тут и memcpy стреляет).

Отвечу сам себе: std::string не TriviallyCopyable.

Продолжу монолог/рассуждения в слух :)


В общем, судя по всему:



UB здесь действительно нет. И единственная потенциальная проблема — "переопределение" конструктора по умолчанию.


Плохо, что в GCC смешали предупреждение для тривиального типов и для trvially-copyable типов.

В вашем примере тип Settings соответствует критерию тривиальности — там конструктор по умолчанию автоматически сгенерированный. Добавьте static_assert(std::is_trivial<Settings>::value), компиляция пройдёт.


Если уберёте второй конструктор (он в коде не используется), то bar() и baz() перестанут отличаться:



А если переключиться на clang, то даже исходный пример перестанет отличаться:



Но ладно, всё равно мой случай более корректно описывает такой пример:



Да, memset() тут выглядит эффективнее, до тех пор, пока конструктор инициализирует поля нулями. В противном случае поведение по умолчанию отличается от задуманного.


Ну далее, как я писал выше, после инициализации структуры происходит заполнение каждого поля. В структуре конструктор по-умолчанию — с аргументами (со значением по-умолчанию), так что код будет выглядеть как-то так:



И тут даже на GCC отличий нет.

НЛО прилетело и опубликовало эту надпись здесь
Конструктор.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Ещё мешает инвариант и когда в конструкторе происходит что-то вроде:


a = 22;
b = 34;
c = 127;

Повторюсь, конкретно в нашем случае, это не так, там нули, т.е. в реальности memset() ничего не ломает, по крайней мере на x86_64 и GCC 9.2 (ну и я не вижу вообще каких-то причин там чему-то сломаться на других связках).

НЛО прилетело и опубликовало эту надпись здесь

Ну мне он даже не совсем мешает. Мне просто любопытно. Немного истории: код давешний и изначально было просто:


memset(&settings, 0, sizeof settings);

Компилятор был GCC версии < 8.0. В версии 8.0 вводят -Wclass-memaccess и ключают его в -Wall.


Происходит переезд на GCC 9.2. Появляется предупреждение. Его фиксят кодом приведённым выше, т.е. явным приведением. Тогда как куда логичнее (ну как логичнее, мне логичнее) было бы убрать memset() и поставить {} после объявления переменной settings.


Т.е. как бы тут и не сломано ничего, и пусть оно так и будет. Но захотелось по-разбираться, а тут повод такой, спецам вопрос задать :)

НЛО прилетело и опубликовало эту надпись здесь
Ну в самом же деле, нельзя же ожидать, чтобы в стандарте были перечислены вообще все допустимые случаи использования любых комбинаций функций из стандартной библиотеки. Там и про memcpy()-то главным образом упоминается в иллюстрационных примерах кода только. Стандартом гарантируется, что trivially copyable типы располагаются в памяти одним непрерывным куском, могут быть скопированы в последовательность char и обратно, и в плане представления в памяти каждый такой тип в совокупности эквивалентен последовательности unsigned char длиной sizeof(T) для совместимости с ISO C memory model, а как там внутри этой последовательности представлены конкретные элементы trivially copyable структуры — это implementation-defined. В совокупности не вижу причин, почему с ними нельзя использовать memset() (с оглядочкой на «implementation-defined», конечно, но это уже такое… разумное ограничение).
НЛО прилетело и опубликовало эту надпись здесь
Ну что поделать, стандарты на большинство языков пока не пишут в каком-то формальном виде, из которого можно было бы вывести какое-то строгое доказательство чего-нибудь. Даже на некоторые «современные и модерновые» языки (не будем показывать пальцем) описание пишется исходя из де-факто имплементации (особенно если компилятор единственный, хехе), в которой регулярно оказываются какие-нибудь дыры. Пока вот так вот.

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

Это только пока кто-нибудь там этот единственный компилятор не форкнул из-за какого-нибудь внутрикомандного конфликта или еще чего-нибудь. А потом будет все то же самое. Без ISO был бы вообще невообразимый пипец, не просто какие-то там «девиации», а вообще какие-нибудь несовместимые реализации, вплоть до того, что в одной реализации некое ключевое слово вполне могло бы означать одно, а в другой — совершенно другое.
Без ISO был бы вообще невообразимый пипец, не просто какие-то там «девиации», а вообще какие-нибудь несовместимые реализации, вплоть до того, что в одной реализации некое ключевое слово вполне могло бы означать одно, а в другой — совершенно другое.

Эм… Как бы и с ISO это не помогает — реверанс в сторону compiler-specific options. Я просто очень долго развлекался сведением к общему знаменателю кода между gcc/msvc/watcom. И, да, это не совсем про С++, но и про него тоже.


А потом будет все то же самое

скорее соглашусь.

НЛО прилетело и опубликовало эту надпись здесь
Знаете, по сравнению с ситуаций, когда у вас банально невозможно написать без использования диалектных отличий простое добавление одной строки в конец другой — всё это мелочами кажется.
НЛО прилетело и опубликовало эту надпись здесь
Да в том же паскале :) Я когда-то оочень давно на нем писал, вот в online fpc налабал простенькую программку:

program Hello(input, output);
var
    s: string[10];
begin
    s := 'AA';
    s := s + 'BB';
    
    writeln(s);
end.

Все компилируется, работает, печатает «AABB», все хорошо. А теперь ставим в начале директиву "{$mode iso}", и начинается секс… :)
Именно. При этом разные диалекты ещё и ведут себя сильно по разному. А стандартный вариант не используется почти нигде (некоторые компиляторы реализуют поддержику — но это так, чисто показать, что «так мы тоже умеем»).
Без ISO был бы вообще невообразимый пипец, не просто какие-то там «девиации», а вообще какие-нибудь несовместимые реализации, вплоть до того, что в одной реализации некое ключевое слово вполне могло бы означать одно, а в другой — совершенно другое.
Собственно в качества примера могу рекомендовать глянуть на Pascal. Лет 20-25 он был зело популярен, даже некоторые весьма популярные операционки на нём писались изначально… а потом — кончился.

Во многом как раз потому что разные реализации не позволяли обмениваться исходниками без соврешенно диких напрягов…
НЛО прилетело и опубликовало эту надпись здесь
Может ещё и историю с gcc 2.96/2.97 застали (которых теоретически не бывает, а практически они вовсю использовались)?

Да, весело было.

Но принципиально было направление движения: после выхода стандарта C++98 разработчики большинства компиляторов взяли стандарт и начали его реализовывать.

Вот в те времена, о которых вы пишите я помню даже тест у нас был и там был вопрос: «сколько C++98-совместимых компиляторов C++ вы знаете». Как я потом выяснил за каждый названный компилятор снимался один балл…

А потом… всё начало «сходиться к стандарту».

Да, процесс занял порядка 10 лет, но ближе к концу нулевых всякие нестандартные фичи стали считаться «дурным тоном».

А вот Pascal — там всё было наоборот: каждый разработчик считал своим долгом расширить всё особым, несовместимым ни с чем способом.

При это «улучшениям» подвергались как раз самые базовые возможности: указатели на функции, работа с файлами, строками… много вы программ, которым ничего этого не нужно видели?

Всё упёрлось, по большому счёту, в то, что каждый разработчик надеялся сам «окучить» всю поляну и срубить бабла со всех разработчиков. Потому что ведь у них же не будет выбора: они завяжутся на его фичи и будут «по гроб» за них платить!

В результате — просто тупо всех разработчиков растеряли.
НЛО прилетело и опубликовало эту надпись здесь
А ещё, кстати, у такого языка, как Ada, был и есть стандарт, тоже уровня ISO.
Дык у Pascal было их аж два. И даже есть такие экзотические вещи, как ECMA-372.

А у Java и Python как раз ISO-стандартов нету.

то есть, каких-то принципиальных отличий де факто от того же паскаля не было.
Было. Но вот, в качестве примера, древний, как говно мамонта, DR-DOS. Читаем:
The following third party tools were used to build the executables. Other versions of these tools may work but have not been tested.

Tool Component
==== =========
Watcom C v7.0 COMMAND
Borland C v2.0 COMMAND
Microsoft MASM v4.0 IBMBIO, COMMAND
Microsoft Link v5.10 IBMBIO, COMMAND
Microsoft Lib v3.0 IBMBIO
То есть даже тогда, когда упоминаются строго конкретные версии каких-то компиляторов — часто упоминаются два, три, а иногда и больше.

Много вы подобного видели в приложении к Pascal или Delphi? Наоборот, скорее вспоминаются фразы «работает только с Turbo Pascal 3» или «Совместимо с Delphi 7, но не Delphi 8».

И, насколько я сейчас могу судить, в ранних нулевых совместимость между компиляторами мало кого волновала
Аж настолько никого не волновала, что куча библиотек поддерживала 2-3, а то и больше компиляторов? Так никому не нужно было, что были созданы Metaconfig и Autoconf?

Нет, какая-то часть участников экосистемы стремилась-таки «перетянуть одеяло на себя» (те же Borland или Watcom)… ну и кончили они примерно там же, где и Pascal (последняя релизнутая версия Watcom, OpenWatcom 1.9 — это 2010й и версия 2, которая всё никак не выйдет, даже не поддерживает C++11).

Но были и другие силы. Был проект GNU и GCC, были имитировавшие их ICC и PGI (правда Intel пытался сразу усидеть на двух стульях имитируя под Windows MSVC, а под Linux GCC).

То есть было достаточно центростремительных сил для того, чтобы язык сохранялся единым. Причём это как раз в нулевые произошло: разработчиков GCC (так-то уже достаточно популярным) даже не спрашивали в принципе во время разработки C++98, а когда разрабатывали C++11 — то было уже понятно, что если что-то не будет поддержано GCC… то этого чего-то, можно считать, в стандарте и нету.

В общем можно долго обсуждать почему разработчики C++ стремились к чему-то единому, а разработчики Pascal или, там, Basic — не стремились… но результат — нагляден и очевиден.

А как раз формальный штамп ISO ничего не даёт: у Pascal их аж целых два — а толку?
наличие конструктора по умолчанию никак не влияет на свойство trivially copyable

Так собственно этот момент меня и поставил в ступор. Ругань именно на non-Trivial тип, который при этом остаётся trivially copyable (TrivialType = TriviallyCopyable && нет-пользовательских-конструкторов-по-умолчанию). И именно memset.


Я вот подумал, вполне можно было бы написать приведение явное, если это вызвано вопросами производительности (на самом деле нет), а перед поставить static_assert(std::is_trivially_copyable_v<Settings>);. Тогда если вдруг в будущем класс станет ещё и non-TriviallyCopyable это свалится на этапе компиляции и защитит тылы. Сейчас же создана ловушка, которая может сработать, а может и не сработать в будущем.

НЛО прилетело и опубликовало эту надпись здесь
Одного конструктора для нетривиальности с точки зрения memcpy/memset маловато. Вот если там еще деструктор виртуальный например добавили, то таки да:

#include <iostream>

struct Trivial {
    int m;
 
    Trivial(int a): m(a) {}
};

struct NonTrivial {
    int m;
 
    NonTrivial(int a): m(a) {}
    virtual ~NonTrivial() {}
};

int main()
{
    std::cout << std::is_trivially_copyable_v<Trivial> << " "
              << std::is_trivially_copyable_v<NonTrivial>;
}

выведет «1 0». Хотя gcc -Wall выдает предупреждение в случае попытки применить memset к переменной типа Trivial (а clang, кстати, нет — он предупреждает только в случае NonTrivial), но в реальности делать memset trivially copyable типам разрешено.

std::is_trivial / std::is_trivial_v выводит 0: https://ideone.com/FCHQwc

НЛО прилетело и опубликовало эту надпись здесь

В данном случае GCC ругается именно на non-trivial тип:


```:13:39: warning: 'void* memset(void*, int, size_t)' clearing an object of non-trivial type 'struct Settings'; use assignment or value-initialization instead [-Wclass-memaccess]

потому я и предположил, что проверка должна быть соответствующей.

А чем, собственно, плох такой код?

Неопределённого поведения в нём нет, если виртуальные функции не используются, и если мы работаем с нормальными платфомами, где bool, nullptr и +0.0 состоят спрошь из битов со значением 0 — то в чём, собственно, подвох?
Могу предположить, что при некоторой комбинации условий оптимизирующий компилятор может просто выкинуть этот memset, с помощью которого автор хотел «затереть» секреты.

www.viva64.com/en/w/v597
Про этот варинт уже Andrey2008 спросил.
если мы работаем с нормальными платфомами, где bool, nullptr и +0.0 состоят спрошь из битов со значением 0

Вообще-то, всё наоборот: перечисленные платформы называют «нормальными» лишь потому, что написано огромное количество таких memset'ов. А потом появляются люди, которые с удивлением узнают, что null pointer и целочисленный ноль — разные вещи. А ведь нормально — это не писать такой код.
Вообще-то, всё наоборот: перечисленные платформы называют «нормальными» лишь потому, что написано огромное количество таких memset'ов.
Нет. Они «нормальные», потому что позволяют использовать .bss. Ну а что после этого можно использовать memset — это уже полезное следствие.
Ну, не будут указатели и числа с плавающей точкой лежать в bss — не велика беда.
Код с потерей информации о типе, коим является memset, — вот настоящая проблема.
Код с потерей информации о типе, коим является memset, — вот настоящая проблема.
В каком месте это проблема, извините?
Ну мой ответ был бы «не знаю, там UB». Без попытки угадать 1,2 или 2,1. Потому что UB потому и UB что может быть что угодно.

ну должны же быть границы у UB (не прям совсем всегда, и у любого, а вот в этом конкретном случае)
Очевидно, что 1, 2 в любых комбинациях может вывести.
А вот если вдруг выведет 0 или 3, да ещё и так, что собеседуемый это обоснует и докажет, а также продемонстрирует — вот тогда да, повод уже почесать репу...

ну должны же быть границы у UB (не прям совсем всегда, и у любого, а вот в этом конкретном случае)
Нету. Вот совсем нету. Стандарт это подчёркивает отдельно:
If any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation)
Фраза в скобочках — это не моя приписка, она в стандарте.

А вот если вдруг выведет 0 или 3, да ещё и так, что собеседуемый это обоснует и докажет, а также продемонстрирует — вот тогда да, повод уже почесать репу...
Ну откуда можется взяться 3 — неизвестно, а вот выкинуть весь код функции — можно было бы вполне.

хм, а разве пример с printf не UB до C++17?


1) If a side effect on a scalar object is unsequenced relative to another side effect on the same scalar object, the behavior is undefined.

на cppreference даже пример приведен:


 f(++i, ++i);       // undefined behavior until C++17, unspecified after C++17

забавно, что разработчики PVS Studio это не читали :))

f(++i, ++i) !== f(i++, i++)

Действительно, и как это я не заметил. Спасибо, товарищ капитан

Так в чём же различие, объясните нам?
И там, и там изменяется одна переменная между двумя точками следования.
++i — сначала инкрементирует, а затем возвращает новое значение
i++ — возвращает текущее значение, а в переменную записывает новое

int i=1;
f(i++); //функция вызовется с параметром 1

int i=1;
f(++i); //функция вызовется с параметром 2


Лол, деточка, торжественно повышаю тебя с капитана очевидности до адмирала ясен х*й.
Наплевать, что там про точки следования написано, да ведь?

Плохо написано.
!== выглядит, как будто это реальная операция, а не "два пути". А в этом случае может сперва выполниться правая часть. Или левая. И сделать неравенство совершенно очевидным.

забавно, что разработчики PVS Studio это не читали

Ещё забавнее, что никто из кандидатов не читал, и все вместе они уверены, что знают ответ :)
С определенного момента, если программа на C++ делает нечто странное — для очистки совести приходится смотреть, что выдает gcc -S. В C и раннем C++ странное поведение программы гарантированно означало, что просто где-то упустили указатель. В целом, старые компиляторы более предсказуемы и более-менее просто переводят в машинные коды то, что написано разработчиком. Новые — с моей точки зрения, чрезмерно смелы в оптимизации. Я воспитан еще в той идеологии, что программисту виднее где оптимизировать, а где — нет. И да, я знаю что этот подход несовместим с шаблонами и автоматической кодогенерацией. Тем хуже для шаблонов.
С определенного момента, если программа на C++ делает нечто странное — для очистки совести приходится смотреть, что выдает gcc -S

А вариант выучить язык, на котором пишете, вы совсем не рассматриваете? Ну, тогда попробуйте санитайзеры.
Я воспитан еще в той идеологии, что программисту виднее где оптимизировать, а где — нет. И да, я знаю что этот подход несовместим с шаблонами и автоматической кодогенерацией. Тем хуже для шаблонов.

Только вот компиляторы развиваются как раз в сторону оптимизации кода, и тем хуже как раз для вас, а не для шаблонов :)
В свое время, посмотрев в какую сторону поехал C++, мы для больших систем перешли на Java. Хотя по #define, typedef и RAII скучали очень.

В итоге, C/C++ остался для программирования ближе к железу (контроллеры). Но там удается оставаться в рамках разумного подмножества языка.

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

Это не значит, что мы совсем не используем шаблоны по причине религиозного неприятия. Скорее, мы их используем в духе раннего Страуструпа для избавления от написания дублирующего кода (то есть генерации нескольких версий класса для разных входящих типов). А вот мета-программирование, шаблонная магия, бусты — извините, но нет.
В свое время, посмотрев в какую сторону поехал C++, мы для больших систем перешли на Java. Хотя по #define, typedef и RAII скучали очень.

Отлично! Всегда радуюсь, когда в среде плюсовиков становится меньше любителей #define
Ба! Это вы еще не видели как препроцессор m4 делает sendmail.cf из sendmail.mc — вот где жесть-то! Препроцессор c/c++ — это просто зайка! :-)

А если без шуток, то опять-таки понятны ограничения #define — ну так и не нужно пытаться делать с ним clever tricks.

#deinfe IR_PIN_HIGH ((PIND & (1<<4))!=0)
...
if(IR_PIN_HIGH) { foobar(); }


Читаемость лучше? Однозначно! Дублирования кода меньше? Меньше! Почему это не применять? Ну да, понятно что можно сделать inline-функцию для того же. Но inline — это же пожелание, а не обязанность компилятора. А в контроллерах бывают места, где надо что-то проверить или дернуть биты в регистрах за определенное число тактов. Что касается этого define — компилятору трудно что-то неправильное с ним сделать.

Вторая супер-способность #define — это обращать куски кода в no-op.

#ifdef DEBUG
#define DEBUG_ONLY(x) x
#else
#define DEBUG_ONLY(x)
#endif
...
DEBUG_ONLY(red_led_toggle());


Ну да, синтаксис не очень… Но если мы компилируем без отладки — вызова red_led_toggle нет в принципе — и никаких проверок в этом месте тоже!

Для сравнения — в Java, даже в лучших реализациях отладочных библиотек (типа Log4j) проверка включения отладки делается в рантайме, и отладочный код всегда присутствует. Невозможно сделать его no-op на этапе компиляции (но там это обычно и не критично).
Вы, наверное, думаете, что рассказали мне что-то, чего я не знаю? Вон сколько текста :)
Жаль вас разочаровывать, но это не так. И тем не менее, даже при том, что иногда попадаются случаи, когда не избежать использования препроцессора, любить тупорылую текстовую подстановку — это какой-то особый изврат. Я и действительно радуюсь, когда любители оного сваливают в другой язык.
У каждого свой взгляд на один и тот же предмет. Та же текстовая подстановка — это начиная от search/replace в редакторе, через sed и до регулярных выражений (которые теперь есть вообще везде). Может быть она тупорылая — но простая, интуитивно понятная, и (eсли не начинать с ее помощью делать слишком умные вещи) весьма надежная. О вкусах спорить не буду. Радует вас что кто-то уходит с языка, ну что ж, в карантин любой повод для радости — ценность. Я радуюсь тому, что в C++ (несмотря на все усилия по его развитию) всегда кто-то остается! :)
Та же текстовая подстановка — это начиная от search/replace в редакторе, через sed и до регулярных выражений (которые теперь есть вообще везде). Может быть она тупорылая — но простая, интуитивно понятная

Ага, именно поэтому говорят «я стал решать проблему с помощью регулярных выражений — теперь у меня две проблемы». Это как парадокс блаба — если вы не использовали синтаксических макросов (в каких-то других языках) и/или не цените статическую типизацию, предоставляемую шаблонами, то мне не объяснить вам чем так плохи макросы. Боюсь, что даже если они ударят вас по лбу, как неаккуратно оставленные грабли (или пониже, если детские), то вы всё равно не поймёте проблемы, потому что не видите альтернативы.
При всем уважении!.. Я пользуюсь препроцессором C, ну где-то с 90-91 года. Уж тридцать лет скоро! Но не помню я каких-то проблем, которые бы это вызывало. И у знакомых тоже нет. Разумеется, если писать сложные выражения где аргумент макроса используется несколько раз — а потом увлекаться операциями с побочными эффектами в аргументах, может быть плохо. "… а вы так не делайте!" © Анекдот
Я пользуюсь препроцессором C, ну где-то с 90-91 года. Уж тридцать лет скоро! Но не помню я каких-то проблем, которые бы это вызывало. И у знакомых тоже нет.

Всё в этой жизни имеет недостатки, но вы их не видите уже в течение 30 лет. Вы хоть отдаёте себе отчёт насколько ваши слова подтверждают парадокс блаба?
Не подменяте понятия, пожалуйста. Я знаю о недостатках препроцессора, и строю деятельность таким образом, чтобы эти недостатки не выливались в проблемы в коде. Должен признаться, что особых неудобств это не вызывает. Скажем, nul-terminated строки в C вызывают гораздо больше раздражения. Или, скажем, отсутствие возможности перегрузки оператора == в Java. Если у кого-то препроцессор на первом месте в списке раздражающих факторов — ну, удивительно для меня конечно — но путь у каждого свой…
НЛО прилетело и опубликовало эту надпись здесь
Объявить интерфейс InputProcessor с чистой виртуальной функцией process_chunk() (и стандартными функциями лайф-цикл менеджмета — init, shutdown, export_stats). Реализовать несколько классов, реализующих этот интерфейс с реализацией единственного алгоритма подсчета в классе. Main разбирает опции командной строки, создает список обработчиков ввода (в зависимости от того, что запрошено), читает порции байт из входного файла и по-очереди кормит их обработчикам. В конце спрашивает каждый обработчик, что он насчитал. Мне кажется, что это более-менее стандартное решение для таких задач. Обращу внимание, что решение со списком обработчиков прекрасно параллелится при необходимости.

Но еще бы сделал два замечания. Во-первых, нужно оценить сложность обработчиков. Накладные расходы на обработку чанка будут как минимум — выбор адреса виртуальной функции и indirect call + ret. Если несколько обработчиков реализуют тривиальные алгоритмы (простое увеличение счетчиков) — их есть смысл запихнуть в один (несколько счетчиков подряд увеличить может быть дешевле чем сделать несколько indirect-call'ов плюс возможные негативные эффекты от нелокальности в кэше и пайплайне процессора, зависящие от продвинутости такового).

Во-вторых, следовало бы добавить InputDecoder. Потому что на вход может идти старый добрый ASCII, wide chars, unicode utf-8, может быть что-то еще. InputDecoder приводит все возможные варианты входа к единому внутреннему представлению. Для задачи wc — достаточно только определять класс символа (word-delimiter, line-delimiter, digit, printable, non-printable, composite). Для другой задачи — может быть перевести все в UNICODE (как в Java) и обработчики ввода писать только для UNICODE-чанков.

А-а, да — ни препроцессор ни шаблоны мне лично тут не нужны. От слова «совсем».
НЛО прилетело и опубликовало эту надпись здесь
Не, ну у меня в коде process_chunk, а не process_char. Мы ж тут типа все умные и понимаем, что и доставание данных откуда-то (файл, БД, network) происходит кусками, и обрабатывать их, стало быть, тоже посимвольно не следует.

Поскольку мы не делаем на этапе дизайна никаких предположений относительно внутреннего устройства InputProcessor — есть полная свобода для оптимизаций. Им не возбраняется иметь свое внутреннее состояние. Если обработчику известно, что на данном процессоре эффективнее всего обрабатывать чанки по 64 байта — пусть большие чанки обрабатывает in-place а мелкие — складирует себе в буфер и обрабатывает когда наберется 64 штуки (условно). Более того, никто не запрещает иметь один и тот же обработчик оптимизированный под разные архитектуры — и подменять его, например выбором динамической библиотеки на этапе загрузки приложения…

Но идея в принципе понятна, и я еще раз повторюсь — религия использовать сложные шаблоны нам не запрещает. Но прежде — смотрим другие варианты, и (как пишут в аннотациях лекарств) «применять, соизмеряя риски и ожидаемую пользу»… Как-то так.

Кстати, внутри вчера обсуждали то что я пишу — родилась альтернативная гипотеза. Мы ж пишем то на Java, то на C/CPP. Чтобы каждый раз не переключать мозг — оно само как-то построилось более-менее общее подмножество языков. Ну потому что #define boolean bool я в хедерах видел — видимо кого-то достало… :-)
Не, ну у меня в коде process_chunk, а не process_char.
То есть вы, фактически, весь wc засунули в одну функцию и предложили эту функцию реализовать 15 раз? Гениально, да.

С таким подходом вообще ничего не нужно, если вы способны порождать десятки копий кода и нигде не ошибиться после copy-paste.

Мы ж тут типа все умные и понимаем, что и доставание данных откуда-то (файл, БД, network) происходит кусками, и обрабатывать их, стало быть, тоже посимвольно не следует.
Вообще-то самое простое — это сделать mmap, получить один длинный кусок и его обработать.

Но прежде — смотрим другие варианты, и (как пишут в аннотациях лекарств) «применять, соизмеряя риски и ожидаемую пользу»… Как-то так.
Да именно как-то так:
$ time wc /lib/x86_64-linux-gnu/libc.so.6
   5541   34419 1820104 /lib/x86_64-linux-gnu/libc.so.6

user	0m0.137s

$ time LC_ALL=C wc /lib/x86_64-linux-gnu/libc.so.6
   5541   34372 1820104 /lib/x86_64-linux-gnu/libc.so.6

user	0m0.033s
Как раз coreutils сделаны по вашим заветам — и дико тормозят. Wc и grep, в частности…

И именно за счёт ваших любимых виртуальностей…
НЛО прилетело и опубликовало эту надпись здесь
Про coreutils — насколько я вижу, для UTF-16 он работает с той же скоростью что и для LC_ALL=«C». Разница в 4 раза возникает из-за символов переменной длины, следовательно. Интересно посмотреть, сохраняется ли она если дать на вход корректный UTF8-файл. Очевидно, что в бинарном файле по крайней мере часть попыток преобразования потока байт через mbrtowc завершаются ошибкой, и оно вынуждено восстанавливать контекст и/или запускать mbrtowc со сдвижкой на один байт, пока снова не запустится корректное преобразование.
Интересно посмотреть, сохраняется ли она если дать на вход корректный UTF8-файл.
Сохраняется. LC_ALL=C — это такой себе «админский» совет для работы с логами.

А там вообще обычно внутри чистый ASCII.
Ну тогда проблема, скорее всего, не в алгоритме расчета как таковом, а в преобразовании многобайтовых символов в wide-chars. Попытавшись подумать за разработчика стандартной библиотеки — у меня сходу нет хорошего решения. Будет приличное количество if-ов, таблицы в памяти, и т.д. То есть имеем сразу в кучу: промахи предсказателя ветвлений, срывы пайплайна, нелокальность данных, и т.д. И ладно бы мы сначала начитали эти символы в приложение, а потом начали с ними что-то разумное делать — потери растворились бы на фоне… А тут по-сути, чтение символа и его преобразование и есть вся работа. КПД хуже паровоза, нафиг…
Ну тогда проблема, скорее всего, не в алгоритме расчета как таковом, а в преобразовании многобайтовых символов в wide-chars.
Именно так. Поскольку писали они на C, то для многобайтовых кодировок у них как раз callback. А униформные по таблице обрабатываются.

КПД хуже паровоза, нафиг…
Почему нафиг? Если вы заинлайните обработку, то всё сведётся к банальной, хорошо предсказуемой, проверке c < 0. Скорость будет почти как у ASCII.

А вот как раз «А-а, да — ни препроцессор ни шаблоны мне лично тут не нужны. От слова «совсем»» — ровно и привело к наблюдаемому эффекту.
Надо смотреть реализацию mbtowc в glibc. Не занимался этим, честно! Но что-то мне кажется, что преобразование UTF-8 — wide char сложно заинлайнить. Особенно, с учетом ситуации что на вход имеют право поступать некорректные UTF-8 последовательности.
Но что-то мне кажется, что преобразование UTF-8 — wide char сложно заинлайнить.
Вам не нужно эту функцию инлайнить. Все символы, на которые вы должны реагировать — вообще однобайтовые. И всё что вам нужно — функция charlen. Она, для UTF-8, тривиальна и вырождается в несколько довольно-таки хорошо предсказуемых ветвления.

Но вот её вызов — «стоит» изрядно.
НЛО прилетело и опубликовало эту надпись здесь
Вот только вы, в этом случае, получите другой результат на wc /lib/x86_64-linux-gnu/libc.so.6.

Устраивает он вас или нет — вопрос философский.
НЛО прилетело и опубликовало эту надпись здесь

Для единственной реализации часто лучше посмотреть на pimpl и не бросаться в абстракции и виртуальность

Ну это само по себе своего рода КОНТРАКТ.
Вы знаете, что какие-то части кода являются макросами, какие-то функциями. Постоянно (30 лет!) держите эти особенности в голове.
Завтра придёт ничего не соображающий джун и запердолит "вызов" макроса с параметром, меняющим стейт. Типа, FOO(C++). И оп-па, всё сломалось, успешной отладки!
Всё же с изначально константными условиями шаблоны обычно справляются не хуже макросов. Но в плюс к этому, смогут и неконстантные условия либо безболезненно переварить, либо выдать ошибку на 5 экранов (это много, но обычно лучше, чем отладочная сессия, покуда займёт всего час, а не полнедели)

Но inline — это же пожелание, а не обязанность компилятора.
Чтобы это стало обязанностью есть __forceinline в MSVC и always_inline в clang/icc/gcc.

Что касается этого define — компилятору трудно что-то неправильное с ним сделать.
Да легко. Вставить сдвиг на 4 прямо в код, например. Или вообще вызвать функцию из библиотеки. Почему вы так уверены, что этого не случится?

Но если мы компилируем без отладки — вызова red_led_toggle нет в принципе — и никаких проверок в этом месте тоже!
Я вас умоляю. if constexpr прекрасно с этим справляются.

#define в другом сильны.

Вот такой простейший #define, никакой шаблонной магией до сих пор не заменить:
#define printintvar(x) printf("\"%s"=%d\n", #x, x);

А вот вот это всё, что «нелюбители шаблонов» устраивают только делают код менее читабельным и сложнее в отладке.
Ну вот, начинаются compiler-specific keywords… :-( Некрасиво это по-моему. С дефайном пока работает так как нужно во всех известных мне компиляторах начиная с BCC 3.1. Ну а если вдруг где-то не сработает оптимизация константных выражений во время компиляции — мне как программисту, не трудно: поставлю в #define предвычисленное значение.

Ну и уж тогда к трюку с printf еще бы добавить наличие predefined-определений __FILE__ и __LINE__. Получается автоматическая идентификация точки печати. Что в Java, так её разэтак, можно сделать только в рантайме через раскрутку стека, и это может быть запрещено полиси java-машины, и руководство предупреждает нас, что это может быть дорогая операция. А в C/CPP — бесплатно и на этапе компиляции!
Ну вот, начинаются compiler-specific keywords… :-( Некрасиво это по-моему.

Староверам сложно с этим смириться, но современные компиляторы лучше программиста знают как оптимизировать код. Нужно лишь им помочь — с помощью опций и корректного кода.
Ну и уж тогда к трюку с printf еще бы добавить наличие predefined-определений __FILE__ и __LINE__. Получается автоматическая идентификация точки печати.

Слава богу, скоро и этот рудимен можно будет выкинуть на помойку. Но те, кто считает, что «плюсы развиваются куда-то не туда», так и будут ворчать, что мир вокруг, оказывается, меняется.
Это философский вопрос. Альтернативная точка зрения была в том, что C был (и остается) успешным — потому что это очень простой и логичный язык. В нем есть свои исторические неудобства типа символьных массивов вместо строк. Но достоинства языка перевешивали. С++ в ранних редакциях решил многие проблемы C. Но потом народу понравилось, и в спецификацию языка начали включать все больше новых и интересных вещей. Мое личное мнение — bloat будет расти экспоненциально. Потому что язык становится сложной системой, где каждая новая возможность будет порождать непредусмотренные side-эффекты. Чтобы их сгладить или устранить — спецификация будет еще расширяться и усложняться. Опять же, мое мнение — что добром это не кончится. К большому счастью, C++ старается сохранять совместимость с предыдущими версиями стандарта (и даже с C, где это возможно). Это дает возможность выбрать разумное подмножество языка (которое сложно чем-то испортить) — и на нем жить без особых проблем.
К большому счастью, C++ старается сохранять совместимость с предыдущими версиями стандарта (и даже с C, где это возможно).

мне кажется, что это и является основной проблемой С++.....

Это дает возможность выбрать разумное подмножество языка (которое сложно чем-то испортить) — и на нем жить без особых проблем.

Проблема в том, что у разных библиотек это подмножество разное.

НЛО прилетело и опубликовало эту надпись здесь
Я уж не говорю о примере khim выше, когда memset эффективнее простой инициализации.
Ну там, всё-таки, был компилятор, который немного подотстал от «переднего края» (gcc сейчас очень мало кто занимается, все ушли на clang, по понятным причинам).

Но вообще векторизация — это боль. Я думаю ещё лет 10 компиляторы не будут уметь прилично код векторизовать.

А научатся — так же, как и со скалярным кодом — когда не будет уж слишком большого количества требований к этому коду.

Приличный 16-битный код, кстати, ни один компилятор до сих пор не умеет генерить — уж очень там вычурно всё (в память могут ходит 4 регистра из 8 и при этом с дикими ограничениями и прочее).
Но вообще векторизация — это боль. Я думаю ещё лет 10 компиляторы не будут уметь прилично код векторизовать.

Я для этого экспериментировал с Интеловским компилятор и был еще такой забавный VectorC. Не знаю сдохли ли они и сколько они сейчас стоят для коммерческого применения. Да и вопрос по поддержке стандартов.
P.S. плачу по трупу Watcom'а

P.S. плачу по трупу Watcom'а
Ну вроде как Watcom ещё, теоретически, существует… но оптимизируе он хуже современных clang'а и gcc.

Когда-то да, он был на коне.

Я для этого экспериментировал с Интеловским компилятор и был еще такой забавный VectorC.
Основная проблема с векторизацией в современном мире — это то, что мы вернулись куда-то в 80е. Когда операции с аккумулятором занимали 1 такт, а с любим другим регистров — 2.

И вот под это никогда не было качественных оптимизаторов. И сейчас нет.

То есть через 10 лет проблемы с векторизацией станут менее актуальны не за счёт прогресса компиляторов, а за счёт того, что процессоры станут более ортогональными. И вам не нужно будет думать какую из десяти инструкций пересылки из регистра в регистр в данном месте использовать.
Вам стоит посмотреть в сторону C#.
Смысл? Для write-once, run everywhere уже есть Java и наработки на ней. Для «быстро и близко к железу» — C/selected CPP subset. Два-с-половиной этих языка закрывают 100% наших потребностей. Ну то есть нет — для всяких одноразовых штук еще есть awk и sed, конечно. :-)

Или это не мне было отвечено?
НЛО прилетело и опубликовало эту надпись здесь
Это не значит, что мы совсем не используем шаблоны по причине религиозного неприятия. Скорее, мы их используем в духе раннего Страуструпа для избавления от написания дублирующего кода (то есть генерации нескольких версий класса для разных входящих типов). А вот мета-программирование, шаблонная магия, бусты — извините, но нет.
Если вы вообще, в принципе, можете их использовать — то это значит, что вы работаете с компилятором, способным вот все эти неочевидные опимизации проделывать.

То есть вы всё равно платите за них — но не используете.

Странное поведение, как по мне. Всё равно что купить мощный спортивный автомобиль и ездить на первой передаче всё время. А то «как бы чего не вышло».
Ну да, шаблонный класс (например, кольцевого буфера). Включается в пару cpp-файлов. Соответственно, создается две специализации шаблона и два .o-файла. Мы не видим, чем бы мы за это платили. Это просто альтернатива тому, что мы бы сделали копию файла ringbuf.cpp и поиском-заменой поменяли указатели на хранимый тип. В данной ситуации шаблон — это опять упрощение и избавление от дублирования кода. При этом никаких выведений типов аргументов, никаких SFINAE — ничего! Компилируется махом. Понимается любым программистом средней квалификации, чего еще нужно?

Продолжая аналогию с автомобилем — от того, что мне дали спортивный автомобиль, мне что теперь дрифтовать и гонять 180 по городу? Ну есть любители, да… А наша цель — скучно и предсказуемо добраться из точки «А» в точку «Б» максимально экономным способом. И да, многие возможности, заложенные конструкторами в машину за те же деньги (бесплатно) при этом остаются не использованными. Ну так это проблема конструкторов, а не моя…

С другой стороны, я видел открытые проекты с последними новинками C++. Для компиляции «с нуля» в пору ставить ферму. Потому что все эти фокусы с шаблонами память жрут как не в себя и время компиляции растет просто удивительно! Но у людей свой путь — надеюсь, они видят в нем какие-то выгоды, которые бы это все оправдывали.
А наша цель — скучно и предсказуемо добраться из точки «А» в точку «Б» максимально экономным способом.
Ага. Конечно.

Почему тогда предложение обновить компилятор встречает такой яростный отпор, если у вас всё «скучно и предсказуемо»?

Рутинная операция, которая у нас случается раз в пару месяцев примерно.

Потому что все эти фокусы с шаблонами память жрут как не в себя и время компиляции растет просто удивительно!
Да, есть такое дело. Но у вас, в общем-то, просто нет выбора: вы просто не сможете поддоживать сколько-нибудь нетривиальное количество кода с вамиши «скучными и предсказуемыми» #define'ами.

А на тех объёмах, где вы можете без шаблонов справитесь (то есть до миллиона строк) — вам ферма в любом случае не нужна будет…
Почему тогда предложение обновить компилятор встречает такой яростный отпор, если у вас всё «скучно и предсказуемо»?

Во-во-во, у этих любителей «разумного подмножества C++» код как правило такой разумный, что ведёт свою собственную жизнь, и потому обновление компилятора приводит к вылезанию всевозможных мест неопределённого поведения. Авторы это знают и всеми силами сопротивляются обновлению компилятора, рассказывая ужасные истории о том, что в нём багов больше, чем мир когда либо видел до этого. Сколько же я этого натерпелся…
Я же говорю: для больших проектов у нас есть Java. Похуже производительность (но с современными JIT — приемлемо), приколы с equals — но зато отсутствие явного управления памятью и отсутствие адресной арифметики сильно понижают порог вхождения. Там где на Java молодой боец уже будет вовсю давать годный продукт — он же на C/CPP будет иметь segmentation fault — core dumped и изучать выдачу valgrind в рабочее время. :-)

И я не говорил, что мы не обновляем компилятор. Наш стандарт с 2008 года Debian Stable с тем компилятором, который там есть. Моя изначальная мысль была, напомню, что при старом компиляторе странное поведение программы было почти 100% результатом ошибки программиста (как правило, в управлении памятью). Сейчас для очистки совести стоит посмотреть, что выдает gcc -S. Язык стал сложный, кодогенерация стала сложной, рамки в которых компиляторам допустимо интерпретировать намерения программиста стали сложными… И меня тут не сильно волнует, кто виноват — дизайнеры языка, авторы компилятора или программист. Очевидно, что программист средней квалификации не в состоянии прочитать и держать в памяти текущий стандарт C++ со всеми его тонкостями. Но работать и решать задачи при этом все-равно нужно. Вывод gcc -S позволяет понять, как именно компилятор понял ваши намерения. Дальше можно искать в стандарте, почему он это сделал и какими словами его направить в более правильную сторону.
Вывод gcc -S позволяет понять, как именно компилятор понял ваши намерения.
Да. Данная, конкретная версия компилятора в данный конкретный момент времени с данными, конкретными ключами компиляции.

Прямой путь к написанию кода, который от малейшего «шевеления» развалится.

Дальше можно искать в стандарте, почему он это сделал и какими словами его направить в более правильную сторону.
Но будет ли «программист средний квалификации» это делать? Вот ведь в чём вопрос.

То что для программиста высокой квалификации взгляд на то, что разные компиляторы генерируют для той или иной конструкции может помочь сделать выбор — очевидно. Но вот как раз «средний» программист, о котором вы так печётесь склонен работать совсем по другому — наплевав на стандарт и руководствуясь подходом: «я посмотрел, что оно работает — и хорошо».

Ну а чем это дальше кончается — всем известно.
НЛО прилетело и опубликовало эту надпись здесь
Возможно, будет интересно, для ninja:
github.com/maxim-kuvyrkov/ninja/commit/4fb7ab82334efcc5cf4fc45664a632a8d9ff0813

Есть и другие патчи которые ограничивают запуск задач в зависимости от потребления памяти (как это делается с CPU например в мастере)
НЛО прилетело и опубликовало эту надпись здесь

Гм, посылать SIGSTOP/SIGCONT?

ограничивало бы компиляцию не по числу задач, а по потреблению памяти.

а это заранее просчитать можно-то? У меня боль и окончание памяти на 8 потоках недавно только сборка QWebEngine вызвала (точнее части с хромиумом), тоже захотелось ограничение по памяти. Я к тому, что начал строить юнит, а его как начало пучить...

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Класс кольцевого буфера, причем без динамического выделения памяти (в контроллерах это зачастую моветон). То есть он садится на статически выделенный массив и реализует абстракцию кольцевого буфера над ним. Специализации для unsigned char и для int. И там и там возврат значения идет в регистре процессора. Собственно, это та часть за которую мы любим C++. Если смотреть код на ассемблере, то можно (закрыв глаза на некоторые признаки) думать, что просто над массивом прямо в коде реализовали кольцевой буфер. Я, в принципе, понимаю что если делать специализацию для сложной структуры, в которой будут свои ссылки на дочерние объекты, и т.д. — это становится нетривиальным. "… а вы так не делайте!" © Анекдот
НЛО прилетело и опубликовало эту надпись здесь

По поводу code bloat, в рабочем проекте (на Cypress FX3) нужно было работать с пачкой I2C устройств. Были две функции, если грубо:


int i2c_read(uint8_t addr, uint16_t reg, void *data, size_t size);
int i2c_write(uint8_t addr, uint16_t reg, const void *data, size_t size);

У устройств куча регистров, причём многие регистры "составные", когда в одном байте по разные биты для разных несвязанных настроек завязаны. И пропереться, записать что-то не то, очень легко.


Изначально была (не моя) реализация, достаточно неплохая в плане выделения отдельных частей (битов) регистра на макросах, но создающая достаточно много объектов в коде, которые потом там и лежали и дёргались в рантайме при обращении.


Плюнул после очередного затыка в нехватку места и после серии выстрелов в ногу от того, что записали не совсем то, что нужно было. Сделал описательную абстракцию над регистрами в виде шаблонов, под каждые (ну почти) значение для регистров позаводил enum class'es, понаставилось тип-трейтов вкупе со static_assert. В результате на этапе разработки описал регистр:


  • к какому типу устройства он применим
  • какой у него адрес
  • размер
  • какие биты используются
  • какой тип значения применим (тот самый enum class)
  • R, W, RW регистр

и всё, шаг влево, шаг вправо — расстрел на этапе компиляции. Да, нужно описывать регистры сначала, да портянка большая получается, да, запись потом в коде тоже не совсем компактная (но, отдать должное — читаешь и понимаешь, что куда пишется). А вот в бинаре… остались только вызовы вышеупомянутых i2c_write()/i2c_read(). Сборка с -O2, компилятор — GCC 4.8.1, т.е. уже древность.


Я это привожу как контр-аргумент про code bloat на шаблонах. Плохо можно на чём угодно написать.


Естественно сначала отладиться пришлось. Не без этого. С сообщениями об ошибках тоже особых проблем не было.

Извините, а можно пример было/стало? А то из описания не воплне ясно, как же вы себе ногу отстреливали раньше.
НЛО прилетело и опубликовало эту надпись здесь
Ну вот тут мне повезло, видимо! :-) У нас системы либо относительно большие, но не требовательные к производительности (управление складом) — либо требовательные, но маленькие (приблуды для конвейера, например). Для больших и требовательных к производительности систем у меня хорошего решения нет. Если деньги позволяют — предложил бы нанимать лучших программистов, давать им современный C++ со всеми плюшками, и надеяться на лучшее… Но даже тут без гарантий…
НЛО прилетело и опубликовало эту надпись здесь
генерировать С (или LLVM IR) руками

А вы уверены, что в вашем генераторе не будет ошибки, которая приведет к генерации кода с UB, хехе?
НЛО прилетело и опубликовало эту надпись здесь
Кстати, да! Коллеги рассказывали, что кусок кода в системе одного банка генерируется по файлу бизнес-правил в Excel (колонки-условия + колонка алгоритма расчета) в простыню из нескольких тысяч строк вида:
if(a && b && c) {
foobar1();
} else if (a && b && d) {
foobar2();
} else if...


Скучно, немолодежно, без шаблонов — но железобетонно и никаких UB. :-)
Что часто и делается в некоторых областях… Однако раньше я от вас слышал обратное утверждение. Если правильно понял тогда. :)

Тут и ответ на вопрос, почему многое продолжают писать на чистом Си.
Ну, я бы аккуратно заметил что начало C++ было неплохое. Например, string после (const) char * гораздо более удобен. А вот cin-cout не зашли. Так и продолжили пользоваться printf-ом. Стандартные коллекции упрощают жизнь во многих случаях. Криками «ура» приветствовали ключевое слово «auto», потому что иначе определения итераторов и проч. всех достали. Почему-то умные указатели не зашли. Ну то есть понятно почему — динамического распределения памяти нет, в коллекциях хранятся исключительно указатели на уже созданные объекты.

Но и на чистом Си тоже вполне пишется, если не надо особых абстракций — а просто жесткую логику реализовать. Как подумаешь, сколько под это надо было раньше корпусов 74 (155) серии поставить… :-)
А вот cin-cout не зашли


Надо же, не только мне не понравилось… Хотя вот std::format облегчит жизнь (а boost::format для читеров).

Собственно я писал, повторюсь, не о том, что Си лучше С++ (это слишком толсто тут), а о том, что схема «DSL -> C» популярна не просто так, и интересно было услышать это и от 0xd34df00d. И он конечно прав, что лучше сразу LLVM IR, но сей байткод сложноват, если надо вдруг «руками полазить», тут уж лучше Си — один из самых простых языков все-таки.
И он конечно прав, что лучше сразу LLVM IR, но сей байткод сложноват, если надо вдруг «руками полазить», тут уж лучше Си — один из самых простых языков все-таки.
Вот только пресловутых UB — в нём не сильно меньше, чем в C++.
Естественно, что тут обсуждать. Однако вряд ли вы предпочли бы ковыряться в байткоде…

А вообще, может так и надо? Давать еще в школе байткод LLVM IR, а далее уже сверху наращивать DSL-ы в зависимости от специализации. Но под это нужно не менее чем решение Политбюро ЦК КПСС. :)
НЛО прилетело и опубликовало эту надпись здесь
поэтому говорить, что С однозначно лучше, я бы не стал


Упаси Боже, этого и я не говорил.

Плюсы на пару порядков сложнее, но в плюсах есть ряд возможностей сложность этого рода как-то спрятать


В целом да, но тут либо нужен талантливый прятатель в команде, либо надо строго не выходить за рамки примитивного сабсета, если у команды есть ограничения.
НЛО прилетело и опубликовало эту надпись здесь
Конечно, просто, на мой взгляд, выбор С это подразумевает


Мы с вами уже это обсуждали, да и без обсуждений очевидно — выбор Си подразумевает одновременно незнание более сложных языков, отсутствие необходимости писать реально большие программы и наличие потенциальной необходимости устраивать ад (в хорошем смысле этого слова). Поэтому так много Си в эмбеде — ад часто нужен, программы небольшие, а на полноценное изучение плюсов у эмбедера (который, особенно в России, делает еще сто дел одновременно) просто не остается времени. Вряд ли кто-то при этом видит реальные плюсы от использования именно чистого Си, ибо кто бы отказался от неймспейсов например? Да никто.
отсутствие необходимости писать реально большие программы

это не очень сильный аргумент. Вот взять то же ядро линукса — это большая программа? Мое мнение — огромная.
C++ начинает играть именно там, когда начинает не хватать возможности описывать сложные взаимосвязи между сущностями — все-таки ООП на коленках в С — это мрак. С другой стороны, я помню плюсы временем 2003-го с MFC… Какой-либо подстраховки разработчика от выстрела в ногу нет. А с плюсами как более мощным инструментом легко себе не просто выстрелить в колено, а оторвать обе ноги


незнание более сложных языков

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

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


Ну вы же понимаете, что имелось в виду не «большая, много строк», а «большая, сложные взаимосвязи». И, да, ядро Линукса в пример, но во-первых там выбора не было, а во-вторых это скорее исключение из общего правила.
Вот взять то же ядро линукса — это большая программа? Мое мнение — огромная.
Во-первых, по современным меркам, она уже не слишком большая, а во-вторых они там очень сильно выходят за рамки C и используют чуть не все расширения, которые были, в принципе, придуманы разработчиками GCC.

Именно потому что им отчаянно не хватает вещей, которые в C++ есть в стандарте.
songlh.github.io/paper/go-study.pdf
просто привел для сравнения размера проектов

Kubernetes — 2MLOC
CockroachDB — 0.5MLOC
etcd — 441KLOC

Почему-то не замахнулись писать на С++… Хотя казалось бы — сетевые приложения должны быть классическим примером использования плюсов.
Очень интересно сравнить с другими большими проектами на С++ и оценить — действительно ли массово пишут много кода на плюсах или это очередной миф.

p.s. варианты — давайте посчитаем исходные коды windows нечестны априори — т.к. там 100500 компонентов и сборная солянка из разных языков — начиная от С и кончая вроде даже паскалем
p.s. варианты — давайте посчитаем исходные коды windows нечестны априори — т.к. там 100500 компонентов и сборная солянка из разных языков — начиная от С и кончая вроде даже паскале
Ну да, разумеется. А давайте ещё исключим все проекты, написанные более, чем одним человеком — и получим вообще нирванну.

Крупные проекты — это всегда «сборная солянка». В том же ядре Linux есть и python и много чего ещё, по мелочи.

И да, они зачастую состоят из отдельных компонент — Android, Chromium, даже какой нибудь Photoshop будут содержать в себе десятки компонент. Что в этом странного-то?

Хотя казалось бы — сетевые приложения должны быть классическим примером использования плюсов.
Во-первых их на плюсах… хватает. Во-вторых как раз под сетевые приложения Go ложится идеально: не нужно общаться со сторонними библиотеками, не нужен GUI (что, в сущности, вариант предыдущего пункта) и очень важна поддержка многопоточности. И то — максимум, что вы накопали — это 2 миллиона строк кода. Это, как бы, размер 3D-движка Unreal Engine (даже не всей игры).

Я даже спорить не буду, что игры — это тоже традиционная вотчина С++

проблема в том, что дальше надо будет смотреть, что делает clang -S. А ещё дальше — cl.exe -S (или что там). Тогда приходит понимание, что дешевле просто устранить причину.

Правильный ответ на любой подобный вопрос: "давайте сначала посмотрим, что скажет компилятор".
А компилятор скажет следующее:


prog.cc: In function 'void F1()':
prog.cc:8:28: warning: operation on 'i' may be undefined [-Wsequence-point]
    8 |   printf("%d, %d\n", i++, i++);
      |                           ~^~
prog.cc:8:28: warning: operation on 'i' may be undefined [-Wsequence-point]

Вот и весь разговор.

Я бы принял такой ответ для любого человека, который не на работу в PVS-Studio устраивается.

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

Я, как человек, знающий C, утверждаю, что этот код печатает полный текст доказательства, что N==NP, и все остальные любопытные вещи, скрытые в гримуаре с названием UB.

Я так понимаю, что версия языка С++ (С++20) — это количество лет, необходимо на изучение этого языка?
Ну знаете. Покажите мне хотя бы одного человека, который C++98 почти сто лет изучал…

Вы так говорите, как будто все эти вещи только в С++20 появились :) Они в языке были почти с самого начала.

Хотел бы коснуться различия поведения gcc и msvs.
Хотелось бы понять почему по разному реализовали порядок передачи парметров в функцию. У мсвс по умолчанию используется порядок fastcall?


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


Я понимаю, что всё это ub. Но Аби никто не отменял.

можно посмотреть все варианты компиляции на
godbolt.org/z/HLNGy3
Я бы на собеседовании ответил так:
Должно распечататься: 1, 1. Все остальное — от лукавого ;)
Поясню.
Функция prinf на момент запуска имеет текущее значение i = 1. С этим значением в качестве параметров она и запускается. Далее (да хоть одновременно с ней) запускаются два оператора i++. Здесь, конечно, явная проблема — два оператора получают одновременный доступ к одной переменной. Это и есть существенная проблема данного кода. Но поскольку в обычной ситуации они запускаются последовательно, то пренебрегаем этой проблемой. Получаем в итоге i = 2;
Вот и все ;).
Получаем в итоге i = 2;
Забавно, что ваше «типа логичное решение» реализовано строго в нуле известных науке компиляторов.

P.S. Только не нужно сейчас искать какой-нибудь tcc и добавлять туда код, который ровно одну эту конкретную функцию скомпилирует так, как вам хочется. Речь-то не об этом…
Вот именно. Речь не о том, как хочется, а о том, как надо.
Думать надо «ширее».
Компилятор — это обычный калькулятор. А все калькуляторы должны работать одинаково. «Забавно» другое: наблюдать обсуждение, что правильнее — 2*2 равно 3 или 5, обсуждая работу только «калькуляторов», но забывая про… арифметику.
Т.е. сначала все же нужно разобраться сколько же будет 2*2 на самом деле…
Поэтому, думаю, соискателей нужно тестировать на знание арифметики, а не на способность шустро давить на клавиши всех типов «калькуляторов». Хотя, не спорю, для кого-то важнее, возможно, именно последнее. Речь об этом. ;)

Арифметика… в школе делить на 0 нельзя, а в универе — можно. В школе sqrt(-1) нельзя, а теории комплексных чисел это мнимая единица.


Арифметика от условий тоже может отличаться. На какой-то платформе эффективнее одно, на другой — другое. На одной платформе поведение было реализовано очень давно и много чего на него завязалось уже, на другой сделали эффективнее, но спустя годы.

От многих знаний — много печали.
Нужно подумать, прежде чем лезть на вышку, когда речь идет о простой арифметике, — есть риск сорваться.
Но мысль интересная — поставить в зависимость от платформы уже и работу калькулятора ;)
Но все же, как представляется, нужно видеть берега. Иначе…
Здесь делили на ноль
image

Работа калькулятора зависит от числа разрядов. Это тоже специфика платформы.
А вот я бы сказал 2 и 2, или даже 2 и 3. Потому что если переводить на уровень Асма, то сначала должен выполнится первый инкремент, потом второй, оба значения будут помещены в стек, а потом уже вызов функции…
Понимаю, что современные компиляторы работают эффективней, но вот так.
Если вы уже начали что-то там «на уровень Асма» переводить, то рекомендую вспомнить, как PDP-11 устроена и про то, что автоинкремент там делала та же инструкция, что и чтение из памяти.

Понимаю, что современные компиляторы работают эффективней, но вот так.
А вот как именно? Почему язык должен вести себя неестественным образом для той архитектуры, на которой он был изначально сделан и также неестественным образом для существующих компиляторов?

Почему ориентиром должно быть что-то, что, если и является, в каком-то смысле, естественным, но не возникает само собой никогда, кроме краткого периода где-то в начале 80х (напоминаю что сегодня на дворе 2020е, а создали C изначально в 1970е… на PDP-11)?
Оказалось, что все еще забавнее. Компиляторы от MS версий 10.00 (VC++ 4.0) и 10.20 (VC++ 4.2) инкрементируют переменную по ходу складывания ее значений в стек. А вот версии 13.00 и старше (версий 11 и 12 у меня нет) инкрементируют ее сразу после возврата из функции, независимо от режимов оптимизации.
Жалко, что godbolt и другие подобные сервисы не содержат старых версий компиляторов. Там много чего интересного, похоже, было.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий