Глубина кроличьей норы или собеседование по C++ в компании PVS-Studio

    Собеседование по C++ в компании PVS-Studio

    Авторы: Андрей Карпов, khandeliants Филипп Хандельянц.

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

    Как и в любой другой программистской компании, у нас есть наборы вопросов для собеседования на вакансии разработчиков на языках C++, C# и Java. Многие вопросы у нас с двойным или тройным дном. За вопросы по C# и Java мы точно сказать не можем, так как у них другие авторы. Но многие вопросы, составленные Андреем Карповым для собеседований по C++, точно сразу задумывались, чтобы прощупать глубину знания особенностей языка.

    На эти вопросы можно дать простой правильный ответ. Можно поглубже, и ещё глубже. В зависимости от этого при собеседовании мы определяем, насколько человек знаком с нюансами языка. Это для нас важно, так как мы разрабатываем анализатор кода и должны очень хорошо понимать языковые тонкости и «приколы».

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

    void F1()
    {
      int i = 1;
      printf("%d, %d\n", i++, i++);
    }

    и спрашиваем: «Что будет напечатано?».

    Хороший вопрос. Сразу много может сказать про знания. Случаи, когда человек вообще не может на него ответить, рассматривать не будем. Такие отсеиваются ещё предварительным тестированием на сайте HeadHunter (hh.ru). Хотя, нет, враки. На нашей памяти была пара уникальных личностей, которые отвечали что-то в духе:

    Этот код напечатает в начале процент, потом d, потом ещё процент, d, потом палочку, n, и затем две единицы.

    Ясно, что в подобных случаях собеседование быстро заканчивается.

    Итак, теперь вернемся к нормальным собеседованиям :). Нередко отвечают так:

    Распечатается 1 и 2.

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

    Нельзя сказать, что именно распечатает этот код. Это неуточнённое (или неопределённое) поведение. Порядок вычисления аргументов не определён. Все аргументы должны быть вычислены до выполнения тела вызываемой функции, но вот в каком порядке это будет происходить, оставлено на усмотрение компилятора. Поэтому код вполне может распечатать как «1, 2», так и наоборот «2, 1». Вообще такой код писать крайне нежелательно, если он собирается как минимум двумя компиляторами, можно и «в ногу выстрелить». И многие компиляторы выдадут здесь предупреждение.

    Действительно, если использовать Clang, то можно получить «1, 2».

    А если использовать GCC, то можно получить «2, 1».

    Когда-то давно мы пробовали компилятор MSVC, и он тоже выдавал «2, 1». Ничего не предвещало беды.

    Недавно для вообще сторонней цели вновь понадобилось скомпилировать этот код с помощью современного Visual C++ и запустить. Собирали под конфигурацию Release с включенной оптимизацией /O2. И, как говорится, нашли приключения на свою голову :). Как думаете, что получилось? Ха! Вот что: «1, 1».

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

    Поскольку стандарт C++ никак не регламентирует порядок вычисления аргументов, компилятор интерпретирует данный вид неуточненного поведения очень своеобразным образом. Давайте взглянем на ассемблерный код, генерируемый компилятором MSVC 19.25 (Microsoft Visual Studio Community 2019, Version 16.5.1), флаг версии стандарта языка '/std:c++14':


    Формально, оптимизатор превратил код выше в следующий:

    void F1()
    {
      int i = 1;
      int tmp = i;
      i += 2;
      printf("%d, %d\n", tmp, tmp);
    }

    С точки зрения компилятора, такая оптимизация не меняет наблюдаемого поведения программы. Глядя на это, начинаешь понимать, что неспроста стандарт C++11 кроме умных указателей добавил также «волшебную» функцию make_shared (а C++14 добавил еще и make_unique). Такой безобидный пример, а тоже может «наломать дров»:

    void foo(std::unique_ptr<int>, std::unique_ptr<double>);
    
    int main()
    {
      foo(std::unique_ptr<int> { new int { 0 } },
          std::unique_ptr<double> { new double { 0.0 } });
    }

    Хитрый компилятор может превратить это в следующий порядок вычислений (тот же MSVC, например):

    new int { .... };
    new double { .... };
    std::unique_ptr<int>::unique_ptr
    std::unique_ptr<double>::unique_ptr
    

    Если второй вызов оператора new бросит исключение, то мы получаем утечку памяти.

    Но вернёмся к изначальной теме. Несмотря на то, что с точки зрения компилятора все хорошо, мы всё равно были уверены, что вывод «1, 1» некорректно считать ожидаемым разработчиком поведением. И тогда мы попробовали скомпилировать исходный код компилятором MSVC с флагом версии стандарта '/std:c++17'. И всё начинает работать так, как и ожидалось, и печатается «2, 1». Взглянем на ассемблерный код.:


    Все честно, компилятор передал в качестве аргументов значения 2 и 1. Но почему все так разительно поменялось? Оказывается, в стандарт C++17 было дописано следующее:

    The postfix-expression is sequenced before each expression in the expression-list and any default argument. The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter.

    Компилятор все так же имеет право вычислять аргументы в произвольном порядке, но теперь, начиная со стандарта C++17, к вычислению следующего аргумента и его побочным эффектам имеет право приступить лишь с того момента, как будут выполнены все вычисления и побочные эффекты предыдущего аргумента.

    Кстати, если тот же пример с умными указателями скомпилировать с флагом '/std:c++17', то и там все становится хорошо – использовать std::make_unique теперь необязательно.

    Вот такое ещё одно измерение глубины в вопросе выяснилось. Бывает теория, а бывает практика в виде конкретного компилятора или разной трактовки стандарта :). Мир C++ всегда сложнее и неожиданнее, чем кажется.

    Если кто-то сможет более точно объяснить происходящее, то просим рассказать в комментариях. Должны же мы окончательно разобраться в вопросе, чтобы хотя бы самим знать ответ на него на собеседовании! :)

    Вот такая познавательная история. Надеемся, было занятно, и вы поделитесь своим мнением по этой теме. И советуем использовать как можно более современный стандарт языка, чтобы меньше удивляться тому, что могут нынешние оптимизирующие компиляторы. А еще лучше – вообще не пишите подобный код :).

    P.S. Можно сказать, что мы «засветили вопрос», и теперь его придётся удалить из вопросника. Не видим в этом смысла. Если человек не поленился перед собеседованием изучить наши публикации, прочитает этот материал и потом использует, то он молодец и заслуженно получит плюсик :).


    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov, Phillip Khandeliants. How Deep the Rabbit Hole Goes, or C++ Job Interviews at PVS-Studio.
    PVS-Studio
    Static Code Analysis for C, C++, C# and Java

    Похожие публикации

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

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

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

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

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

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

              +3
              Не хочу с этим языком иметь ничего общего

              Я тоже. У меня вообще, если честно, какой-то лютый баттхёрт и выгорание от плюсов.


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


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

              Спокойно можно. Это относительно базовые вещи, да и рядом с memset они ошиблись.

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

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

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

                  Причем некоторым статическим анализаторам, например, если не ошибаюсь, SonarQube, не понравилось бы, если бы инициализация i выполнялась в user-provided default constructor — он бы завопил, что его нужно выкинуть, а инициализацию производить в in-class initializer.
                    +2
                    А есть в каких-то компиляторах закладка на это?

                    То есть, вы предлагаете помнить не только стандарт, но и то, как какие компиляторы себя ведут?


                    А закладываться на то, что закладок нет, я бы не стал. Вон, в релизнотесах каждой следующей версии LLVM написано, мол, что теперь LLVM начинает использовать такое-то и такое-то UB для оптимизаций. И тут приходится либо надевать шляпу разработчика компиляторов и пытаться прикинуть, насколько данное конкретное UB типа как безопасно и вряд ли будет использоваться компиляторами, либо обновлять компилятор с болью и страданиями.


                    ЧСХ на моём последнем месте люди к UB относились серьёзно, и, например, когда я написание обёртки для mmap-стореджа над POD'ами типа интов потратил не час, а два дня, сказали не «да ты офигел время продалбывать», а «спасибо, мы не знали, что тут просто скастовать небезопасно, и надо приседать с memcpy и placement new, прикольно получилось, live and learn».

                      0
                      То есть, вы предлагаете помнить не только стандарт, но и то, как какие компиляторы себя ведут?
                      В случаях, когда стандарт что-то, что ранее было нелегальным сделал легальным (и почему-либо нельзя перейти на последнюю версию стандарта) — это разумно.

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

                      Но да, что со всем этим делать — непонятно. UBSAN ловит далеко не всё…
                        0
                        Но да, что со всем этим делать — непонятно. UBSAN ловит далеко не всё…

                        А я и не знаю. Буду проститу… ресёрчером.


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

                  –2
                  Их и не надо в голове держать.
                  Нормально пиши — нормально будет.
                    0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

                                0
                                del
                                  0

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


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

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

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


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


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


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


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


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


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


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


                                      Foo settings{};
                                        0

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

                                          +1
                                          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.
                                            0

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

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

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

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

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

                                              Хотя это «экономия на спичках», конечно.
                                                0
                                                Хотя это «экономия на спичках», конечно.

                                                Не так уж на спичках.


                                                Я тут рандомно тыкнул в известных табличках Агнера Фога в данные по хазвеллу — throughput у мува imm → mem — 1, а у movaps — 0.5, латентность обоих — 3. То есть, если я правильно понимаю, четыре мува сожрут 1×4+3 = 7 тактов, а два movaps — 0.5×2 + 3 = 4 такта. Почти двукратная разница.


                                                Ну или, если реордерер совсем умный и видит через call, и с функцией повезло, то разница вообще в 4 раза (latency можно выкинуть).

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

                                                  Но, с другой стороны, если memset никогда не хуже и иногда лучше — то почему бы и нет?
                                                    0
                                                    Но, с другой стороны, если memset никогда не хуже и иногда лучше — то почему бы и нет?

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

                                                  0
                                                  не вызывает предупреждений у 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 как раз понятно. Он эффективнее.

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

                                                    0

                                                    Неиспользуемая переменная или = вместо == в if тоже вызовут ворнинг, но UB это не является.

                                                      0

                                                      Я потому тему и поднял, потому как упоминание 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 больше.

                                                        0

                                                        И таки в стандарте я пока не откопал нужной строчки, пока где-то рядом, да около. Сам 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 стреляет).

                                                          0

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

                                                          0

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


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



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


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

                                                        0

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


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



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



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



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


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



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

                                                      +1
                                                      Он промолчит про случай SettingsA, хотя формально так делать нельзя.

                                                      Почему нельзя? Можно.


                                                      Мы вам перезвоним.

                                                        –1
                                                        Конструктор.
                                                          +1

                                                          Сможете привести цитату из стандарта, говорящую, что наличие определённого пользователем конструктора по умолчанию ломает memset?

                                                      0
                                                      ЕМНИП, memset и memcpy на нетривиальные типы — это неопределённое поведение.

                                                      Все чуть сложнее. Есть понятие trivially copyable type, и ему мешают только те конструкторы, которые copy/move и определены пользователем (на самом деле ещё сложнее, но я пишу с планшета и примеры кода тут набивать ну такое). Так вот, делать memset значению trivially copyable type можно. Даже если у него есть нетривиальный конструктор по умолчанию.

                                                        0

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


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

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

                                                          0

                                                          Он мешает вам как человеку, но не мешает с точки зрения стандарта языка.


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

                                                            0

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


                                                            memset(&settings, 0, sizeof settings);

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


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


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

                                                              0

                                                              Судя по реакции (не вашей) на мои предыдущие комменты, придётся таки полезть в стандарт. Люблю запах страниц стандарта по утрам!


                                                              Давайте с memcpy, с ним проще. Я смотрю по C++17 draft, но для простоты и тех, кто не хочет грузить 1500-страничные пдфки, даю ссылки на онлайн-версию, где стандарт уже немного другой.


                                                              Так вот, 6.8/2 и /3 говорят, что вы можете делать memcpy для объектов trivially copyable типов (в частности, если они не являются подобъектами), даже если они не содержат валидного значения. И как мы видим из 11.2/1, наличие конструктора по умолчанию никак не влияет на свойство trivially copyable.


                                                              Надеюсь, этого достаточно, чтобы разобраться с memcpy.


                                                              А вот с memset всё интереснее. Он в стандарте упоминается всего несколько раз, и все эти разы — без описания семантики. Поэтому вопрос о том, что разрешено делать с memset, является совсем неочевидным. Ответ «memset в коде на плюсах использовать нельзя вообще», похоже, стандарту не противоречит.


                                                              И да, наличие в конструкторе операций присваивания ничего не значит, потому что никакие инварианты они не устанавливают (по крайней мере, пока вы не защитили поля private, но про это речи не идёт).

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

                                                                  Перечислять не обязательно. Достаточно сформулировать систему правил, из которых можно вывести, что well-typed/well-behaved, а что нет. Если задача сформулировать «на какие значения можно натравливать memcpy и когда» оказывается неподъёмной, то с плюсами всё ещё хуже, чем я последнее время думаю.


                                                                  В совокупности не вижу причин, почему с ними нельзя использовать memset() (с оглядочкой на «implementation-defined», конечно, но это уже такое… разумное ограничение).

                                                                  Я тоже не вижу (особенно если вы это значение потом не используете, и на всякие там trap values вам плевать). Но доказать это на допустимом для меня уровне не могу.

                                                                    0
                                                                    Ну что поделать, стандарты на большинство языков пока не пишут в каком-то формальном виде, из которого можно было бы вывести какое-то строгое доказательство чего-нибудь. Даже на некоторые «современные и модерновые» языки (не будем показывать пальцем) описание пишется исходя из де-факто имплементации (особенно если компилятор единственный, хехе), в которой регулярно оказываются какие-нибудь дыры. Пока вот так вот.
                                                                      0

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

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

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


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

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

                                                                            +1

                                                                            А с ISO тоже по факту пипец, особенно если вы пишете, не знаю, требовательный к производительности код, например. То __forceinline, то #pragma unroll, то интринсики, то __attribute__((target("avx"))), то какой-нибудь там __builtin_assume_aligned или как его.

                                                                              0
                                                                              Знаете, по сравнению с ситуаций, когда у вас банально невозможно написать без использования диалектных отличий простое добавление одной строки в конец другой — всё это мелочами кажется.
                                                                                0

                                                                                А это где так?

                                                                                  +1
                                                                                  Да в том же паскале :) Я когда-то оочень давно на нем писал, вот в online fpc налабал простенькую программку:

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

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

                                                                              Во многом как раз потому что разные реализации не позволяли обмениваться исходниками без соврешенно диких напрягов…
                                                                                0

                                                                                Я как раз лет 17 начинал писать на плюсах, и я помню славные времена VC6, gcc 3-ей ветки, и так далее. Ну и ещё буйство пары десятков других компиляторов, включая AIX'овый и SunOS'овый (с последними я даже имел (не)счастье работать не так давно).

                                                                                  0
                                                                                  Может ещё и историю с gcc 2.96/2.97 застали (которых теоретически не бывает, а практически они вовсю использовались)?

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

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

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

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

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

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

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

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

                                                                                  В результате — просто тупо всех разработчиков растеряли.
                                                                                    +1
                                                                                    Может ещё и историю с gcc 2.96/2.97 застали (которых теоретически не бывает, а практически они вовсю использовались)?

                                                                                    Самому пользоваться не пришлось уже, к счастью, но записи в ченджлогах буста и ACE, мол, что починена совместимость с 2.95.2, помню. А кто сейчас помнит ACE, кстати? А я что-то на днях в приступе ностальгии смотрел — оно ещё вроде даже как-то полуживо, релизится там иногда.


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

                                                                                    Но в ранних нулевых это всё ещё было месиво. И, насколько я сейчас могу судить, в ранних нулевых совместимость между компиляторами мало кого волновала (кроме авторов некоторых библиотек, хотя и
                                                                                    сейчас она мало кого волнует, если выкинуть публичный гитхаб для самоудовлетворения, «УМВР ЧЯДНТ») — то есть, каких-то принципиальных отличий де факто от того же паскаля не было. Хотя я тогда был совсем глупый, мог что-то не так понять или запомнить.


                                                                                    Но, опять же, в том же начале нулевых, паскаль уже был какой-то полудохлый, а на дельфи в моём окружении (школьный препод по плюсам, RDSN, какие-то рассылки) смотрели как на… ну понятно, как. Но, опять же, это у меня такое впечатление было.


                                                                                    А ещё, кстати, у такого языка, как Ada, был и есть стандарт, тоже уровня ISO. Как-то не помогло.

                                                                                      0
                                                                                      А ещё, кстати, у такого языка, как 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 их аж целых два — а толку?
                                                                      0
                                                                      наличие конструктора по умолчанию никак не влияет на свойство trivially copyable

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


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

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

                                                                        Это наиболее правильное решение.


                                                                        Мы на прошлом месте все важные данные обкладывали подобными ассертами, ну и заодно такими, что alignment по кешу, что sizeof меньше или равен кешлайну, и так далее.

                                                              0
                                                              Одного конструктора для нетривиальности с точки зрения 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 типам разрешено.
                                                                0

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

                                                                  0

                                                                  А надо std::is_trivially_copyable, именно он связан с возможностью memset/memcpy.

                                                                    0

                                                                    В данном случае 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]

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

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

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

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

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

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

                                                                  0
                                                                  ну должны же быть границы у 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 — неизвестно, а вот выкинуть весь код функции — можно было бы вполне.
                                                              +12

                                                              хм, а разве пример с 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 это не читали :))

                                                                0
                                                                f(++i, ++i) !== f(i++, i++)
                                                                  –2

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

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

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

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


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

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

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

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

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

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

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

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

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

                                                                            Отлично! Всегда радуюсь, когда в среде плюсовиков становится меньше любителей #define
                                                                              +1
                                                                              Ба! Это вы еще не видели как препроцессор 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 на этапе компиляции (но там это обычно и не критично).
                                                                                0
                                                                                Вы, наверное, думаете, что рассказали мне что-то, чего я не знаю? Вон сколько текста :)
                                                                                Жаль вас разочаровывать, но это не так. И тем не менее, даже при том, что иногда попадаются случаи, когда не избежать использования препроцессора, любить тупорылую текстовую подстановку — это какой-то особый изврат. Я и действительно радуюсь, когда любители оного сваливают в другой язык.
                                                                                  0
                                                                                  У каждого свой взгляд на один и тот же предмет. Та же текстовая подстановка — это начиная от search/replace в редакторе, через sed и до регулярных выражений (которые теперь есть вообще везде). Может быть она тупорылая — но простая, интуитивно понятная, и (eсли не начинать с ее помощью делать слишком умные вещи) весьма надежная. О вкусах спорить не буду. Радует вас что кто-то уходит с языка, ну что ж, в карантин любой повод для радости — ценность. Я радуюсь тому, что в C++ (несмотря на все усилия по его развитию) всегда кто-то остается! :)
                                                                                    +1
                                                                                    Та же текстовая подстановка — это начиная от search/replace в редакторе, через sed и до регулярных выражений (которые теперь есть вообще везде). Может быть она тупорылая — но простая, интуитивно понятная

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

                                                                                        Всё в этой жизни имеет недостатки, но вы их не видите уже в течение 30 лет. Вы хоть отдаёте себе отчёт насколько ваши слова подтверждают парадокс блаба?
                                                                                          0
                                                                                          Не подменяте понятия, пожалуйста. Я знаю о недостатках препроцессора, и строю деятельность таким образом, чтобы эти недостатки не выливались в проблемы в коде. Должен признаться, что особых неудобств это не вызывает. Скажем, nul-terminated строки в C вызывают гораздо больше раздражения. Или, скажем, отсутствие возможности перегрузки оператора == в Java. Если у кого-то препроцессор на первом месте в списке раздражающих факторов — ну, удивительно для меня конечно — но путь у каждого свой…
                                                                                            +1

                                                                                            Такой вот вопрос по мотивам.


                                                                                            Предположим, есть функция, которая, ну, например, реализовывает основной цикл юниксовой утилиты wc. Она умеет считать слова, байты, символы, строки, максимальную длину строки и ещё что-то. Некоторые вычисления дешёвые (например, количество строк — тупо считаем количество \n), некоторые — дорогие (корректно посчитать слова не очень тривиально). Соответственно, возникает желание написать N=5-6 отдельных функций на каждую статистику, нагенерировать 2^N — 1 вариантов общей функции, которая бы вызывала какое-то подмножество из этих, и дёргать конкретный вариант в зависимости от того, что пользователь выбрал посчитать.


                                                                                            На шаблонах это относительно реализуемо (хоть и вырвиглазно). Как это сделать на препроцессоре?


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

                                                                                              –1
                                                                                              Объявить интерфейс 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-чанков.

                                                                                              А-а, да — ни препроцессор ни шаблоны мне лично тут не нужны. От слова «совсем».
                                                                                                +1
                                                                                                Объявить интерфейс InputProcessor с чистой виртуальной функцией process_chunk() (и стандартными функциями лайф-цикл менеджмета — init, shutdown, export_stats). Реализовать несколько классов, реализующих этот интерфейс с реализацией единственного алгоритма подсчета в классе. Main разбирает опции командной строки, создает список обработчиков ввода (в зависимости от того, что запрошено), читает порции байт из входного файла и по-очереди кормит их обработчикам. В конце спрашивает каждый обработчик, что он насчитал. Мне кажется, что это более-менее стандартное решение для таких задач. Обращу внимание, что решение со списком обработчиков прекрасно параллелится при необходимости.

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


                                                                                                Если несколько обработчиков реализуют тривиальные алгоритмы (простое увеличение счетчиков)

                                                                                                Их два: счётчик байт и счётчик строк. Остальные уже нетривиальны. У вас всё ещё получается 15 вариантов.


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


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

                                                                                                Договорившись, что вы работаете только с уникодом или ASCII, можно сильно упростить эту задачу.


                                                                                                Кстати, про чанки вы тоже хорошо заметили, там может получиться, что у каждого алгоритма может быть больше одной версии, в зависимости от того, чанки какого размера допустимы (скажем, строки спокойно считаются по 16-32-64 байт за раз одной векторной инструкцией, со словами это уже ну очень нетривиально, я бы не осилил, с максимальной длиной строки — аналогично), и имеет смысл посчитать итоговый размер чанка как максимум от допустимых для выбранных опций.


                                                                                                А-а, да — ни препроцессор ни шаблоны мне лично тут не нужны. От слова «совсем».

                                                                                                Они полезны, чтобы на этапе компиляции посчитать то, что можно посчитать на этапе компиляции.


                                                                                                Собственно, почему wc — я уже почти дооформил русскоязычный вариант статьи, где описывается, как это можно сделать (на хаскеле, в каком-то смысле эмулируя шаблоны, что как-то даже иронично получается). Как я понимаю, на С аналогичных характеристик производительности без какой-то внешней кодогенерации не получить никак.

                                                                                                  0
                                                                                                  Не, ну у меня в коде process_chunk, а не process_char. Мы ж тут типа все умные и понимаем, что и доставание данных откуда-то (файл, БД, network) происходит кусками, и обрабатывать их, стало быть, тоже посимвольно не следует.

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

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

                                                                                                  Кстати, внутри вчера обсуждали то что я пишу — родилась альтернативная гипотеза. Мы ж пишем то на Java, то на C/CPP. Чтобы каждый раз не переключать мозг — оно само как-то построилось более-менее общее подмножество языков. Ну потому что #define boolean bool я в хедерах видел — видимо кого-то достало… :-)
                                                                                                    0
                                                                                                    Не, ну у меня в коде 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, в частности…

                                                                                                    И именно за счёт ваших любимых виртуальностей…
                                                                                                      +1

                                                                                                      coreutils-то как раз без всяких виртуальностей, они просто (почти) всё считают (и поэтому выглядят квёло).


                                                                                                      С другой стороны, если мы таки возьмём версию с экзистенц… простите, виртуальными функциями, но будем гонять её на чанках в 32 килобайта (сколько влезает в L1 на моём процессоре), то мы сделаем примерно 60 тыщ лишних вызовов на тестовый файл, что при оценке в 7 наносекунд на вызов даёт оверхед в 0.4 миллисекунды, или, учитывая общее время работы порядка секунды, оверхед в 0.04%. В принципе, с этим можно жить.


                                                                                                      Но тут просто повезло, что можно задачу разбить на чанки.

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

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

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

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

                                                                                                                  Но вот её вызов — «стоит» изрядно.
                                                                                                                    +1
                                                                                                                    И всё что вам нужно — функция charlen. Она, для UTF-8, тривиальна и вырождается в несколько довольно-таки хорошо предсказуемых ветвления.

                                                                                                                    Не нужен вам charlen и ветвления, в рамках этой задачи
                                                                                                                    считать те же уникодовые символы в utf-8

                                                                                                                      computation = ByteOnlyComputation $ \cnt c -> cnt + 1 - fromIntegral (((c .&. 0b10000000) `shiftR` 7) .&. (1 - ((c .&. 0b01000000) `shiftR` 6)))
                                                                                                                      0
                                                                                                                      Вот только вы, в этом случае, получите другой результат на wc /lib/x86_64-linux-gnu/libc.so.6.

                                                                                                                      Устраивает он вас или нет — вопрос философский.
                                                                                                                  +1
                                                                                                                  А униформные по таблице обрабатываются.

                                                                                                                  Только тоже тормозят.

                                                                                                      0

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

                                                                                                    0

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

                                                                                          +2
                                                                                          Но 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);
                                                                                          

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

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

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

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

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

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

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

                                                                                                    +1
                                                                                                    Староверам сложно с этим смириться, но современные компиляторы лучше программиста знают как оптимизировать код. Нужно лишь им помочь — с помощью опций и корректного кода.

                                                                                                    А это, кстати, тоже не всегда.


                                                                                                    Как мне на современном стандартном C++ переписать этот цикл, чтобы там была и векторизация, и cmpestrm?


                                                                                                    unsigned long fps_count_naive(unsigned char *str, unsigned long len, unsigned char w) {
                                                                                                        unsigned long c;
                                                                                                        for (c = 0; len-- != 0; ++str)
                                                                                                            if (*str == w)
                                                                                                                ++c;
                                                                                                        return c;
                                                                                                    }

                                                                                                    Усложним задачу: cmpistrm работает быстрее cmpestrm, но требует отсутствия нулевых байт в сравниваемых строках. Как мне сказать об этом компилятору?


                                                                                                    Иными словами, как мне сделать так, чтобы ручной долбёж SIMD тут, в такой банальной задаче (то есть, серьёзно, это тривиальнейший код), был не нужен?


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


                                                                                                    Я уж не говорю о примере khim выше, когда memset эффективнее простой инициализации.

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

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

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

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

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

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

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

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

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

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

                                                                                                      Или это не мне было отвечено?
                                                                                                  0
                                                                                                  А в контроллерах бывают места, где надо что-то проверить или дернуть биты в регистрах за определенное число тактов.

                                                                                                  А вы уверены, что компилятор свернет 1<< 4 в константу во время компиляции?


                                                                                                  А, тьфу ты, за меня уже выше написали. Great minds think alike!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                                                          Ну а чем это дальше кончается — всем известно.
                                                                                                          +1
                                                                                                          А на тех объёмах, где вы можете без шаблонов справитесь (то есть до миллиона строк) — вам ферма в любом случае не нужна будет…

                                                                                                          20-50 тыщ строк, время сборки всего проекта — час в 10 потоков. 10 потому, что на некоторых TU компилятор жрал 6-8 гиг, а на машине было всего 64 гига памяти.


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

                                                                                                            0
                                                                                                            Возможно, будет интересно, для ninja:
                                                                                                            github.com/maxim-kuvyrkov/ninja/commit/4fb7ab82334efcc5cf4fc45664a632a8d9ff0813

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

                                                                                                              Тут проблема в том, что потребление памяти компилятором растёт постепенно, и если у вас одновременно запустилось пусть даже всего 10 задач, которые будут компилироваться 5 минут и сожрут по 7 гигов каждая, то это уже будет больно. То есть, надо как-то собирать статистику, какие TU сколько памяти жрут, и шедулить их на основании заранее собранной статистики.

                                                                                                                0

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

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

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

                                                                                                                0

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

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

                                                                                                            Ну, как минимум, если вы хотите это делать оптимально, то вам надо учитывать, имеет ли тип noexcept-конструкторы, можно ли его безопасно мувить (либо вы опять рассчитываете на оптимизацию, что иронично), и так далее. Привет, sfinae, привет, if constexpr.

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

                                                                                                                Я просто тоже относительно недавно реализовывал что-то, похожее на кольцевой буфер, тоже без динамического выделения памяти (в HFT это зачастую моветон ;)). И там обо всём этом думать приходилось, чтобы писать код без UB.

                                                                                                              +2

                                                                                                              По поводу 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 на шаблонах. Плохо можно на чём угодно написать.


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

                                                                                                                0
                                                                                                                Извините, а можно пример было/стало? А то из описания не воплне ясно, как же вы себе ногу отстреливали раньше.
                                                                                                            0

                                                                                                            А на чем пишете системы, требовательные к производительности?


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

                                                                                                              0
                                                                                                              Ну вот тут мне повезло, видимо! :-) У нас системы либо относительно большие, но не требовательные к производительности (управление складом) — либо требовательные, но маленькие (приблуды для конвейера, например). Для больших и требовательных к производительности систем у меня хорошего решения нет. Если деньги позволяют — предложил бы нанимать лучших программистов, давать им современный C++ со всеми плюшками, и надеяться на лучшее… Но даже тут без гарантий…
                                                                                                                +2

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


                                                                                                                И в настоящий момент, в частности, думаю, что если деньги позволяют, то лучше обмазываться предметно-специфичными языками и генерировать С (или LLVM IR) руками.

                                                                                                                  0
                                                                                                                  генерировать С (или LLVM IR) руками

                                                                                                                  А вы уверены, что в вашем генераторе не будет ошибки, которая приведет к генерации кода с UB, хехе?
                                                                                                                    +2

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

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


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

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

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


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

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

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

                                                                                                                          Только в C тоже есть UB и куча возможностей пострелять себе по ногам (посмотрите на количество уязвимостей из-за того, что где-то вышли за границы массива, или strcpy там не туда что-то скопировал, или обратились по уже дохлому указателю), поэтому говорить, что С однозначно лучше, я бы не стал.


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

                                                                                                                            0
                                                                                                                            поэтому говорить, что С однозначно лучше, я бы не стал


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

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


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

                                                                                                                              Конечно, просто, на мой взгляд, выбор С это подразумевает, так что это пишущие на чистом С говорят :)


                                                                                                                              А лично я бы при прочих равных выбрал бы таки плюсы, если выбор между ними и С, даже при всём моём негативе.


                                                                                                                              Но лучше себя не ставить в такую ситуацию.