Комментарии 208
Мы сами не ожидали, что такое может быть.
Отличный пример того, как не надо относиться к неуточнённому/неопределённому поведению.
Были ли люди, которые ответили, что может вывести и 1, 1? Спорили ли вы с ними, что не может? (:
И да, в C++17 это убрали, так как смысла в этом нету: любая программа, которую можно таким образом «ускорить» — некорректна… а смысл ускорять некорректную программу?
И да, в C++17 это убрали, так как смысла в этом нету: любая программа, которую можно таким образом «ускорить» — некорректна… а смысл ускорять некорректную программу?
только внезапно эти некорректные программы стали корректными. Черт. Не хочу с этим языком иметь ничего общего — это ж надо догадаться наплодить столько деталей, что ни один вменяемый человек в голове их не удержит (что и доказывает эта статья, т.к. обвинить авторов анализатора в непрофессионализме попросту невозможно)
И забавно, что когда даже делают лучше (например, теперь memcpy и прочие начинают лайфтаймы некоторых видов объектов), по факту делают и хуже (так как теперь надо помнить, каких именно объектов, и начиная с какой версии C++, и старое поведение забывать нельзя).А есть в каких-то компиляторах закладка на это?
По моему как раз тут успели вовремя: разработчики компиляторов эту недоговорку не успели истолковать в свою пользу, так что можно считать что во всех стандартах всё как в последнем.
Меня больше радуют фанаты подхода «если компилятор ругается — то всё плохо». Ну вот, например… компилятор ругается — и где ж там, собственно, проблема?
Ну вот, например… компилятор ругается — и где ж там, собственно, проблема?
Причем некоторым статическим анализаторам, например, если не ошибаюсь, SonarQube, не понравилось бы, если бы инициализация i выполнялась в user-provided default constructor — он бы завопил, что его нужно выкинуть, а инициализацию производить в in-class initializer.
То есть, вы предлагаете помнить не только стандарт, но и то, как какие компиляторы себя ведут?В случаях, когда стандарт что-то, что ранее было нелегальным сделал легальным (и почему-либо нельзя перейти на последнюю версию стандарта) — это разумно.
спасибо, мы не знали, что тут просто скастовать небезопасно, и надо приседать с memcpy и placement new, прикольно получилось, live and learnНу про эту же историю написано всё в описании std::bit_cast.
Но да, что со всем этим делать — непонятно. UBSAN ловит далеко не всё…
Нормально пиши — нормально будет.
Так для того, чтобы "нормально писать", эти детали держать в голове и надо.
Собственно даже пример из статьи — зачем так писать? Мне не нужно знать UB там или какое-то конкретное поведение. Я просто не будут так писать.
Либо ты знаешь как работает конструкция и используешь её, либо не знаешь и не используешь. Всё очень просто.
Проблемы здесь только у тех, кто бездумно копипастит код со стэоверфлоу и из других источников.
Либо ты знаешь как работает конструкция и используешь её, либо не знаешь и не используешь.
Не хватает третьего варианта: думаешь, что знаешь, как работает.
«Не думаешь что знаешь», а «Догадываешься»
С++ не рокет сайнс и в рамках стандарта он предсказуем, один раз прочитал ииспользуешь и знаешь как работает.
Еще раз: никто не заставляет лезть в сложные конструкции. Не уверен — не используй.
Это вообще бич современных программистов. Я не так давно с этим столкнулся. Мне повезло, в течении 15 лет я работал со специалистами как минимум не ниже меня по уровню. Очень привык. А тут довелось поработать в команде с джунами, которые при этом дозволено без ревью комитить. Использование «догадок» постоянно. Вместо того чтобы разобраться как работает — делается по принципу «попробую все варианты, какой заработает, тот и оставлю». И вот этого С++ не прощает. Потому что догадки не имеют отношения к реальности. Ты либо знаешь и используешь, либо не знаешь и не используешь. Третий варианто только один — идиот, которые использует не зная, а «догадываясь».
Может хватит в угадайку играть?
Простите, наболело.
С++ не рокет сайнс и в рамках стандарта он предсказуем, один раз прочитал ииспользуешь и знаешь как работает.
ага, только вот обилие стандартов и их имплементаций… Впечатляет. Поэтому, наверное, нужно договориться заранее — какой используете
Есть очень простое правило: в написанном тобой коде ты должен понимать что делает КАЖДАЯ строчка, а вернее даже каждый символ написанный тобой. Зачем ты его написал и как он себя ведет.
Не надо бездумно копипастить чужой код и будет счастье.
Да, если человек не способен следовать этому простому правилу — не надо лезть в IT-вообще.
Есть очень простое правило: в написанном тобой коде ты должен понимать что делает КАЖДАЯ строчка, а вернее даже каждый символ написанный тобой. Зачем ты его написал и как он себя ведет.Это прошлый век, сейчас так никто не пишет. В больших проектах это зачастую просто невозможно — так как вы используете компоненты, о точном предназначении которых можно только догадываться.
Ещё знать язык, которые вы используете (даже такой сложный, как C++) — можно и нужно, все библиотеки (включая сюда и предоставляемые OS сервисы)… невозможно в принципе.
Не надо бездумно копипастить чужой код и будет счастье.А как быть, если в описании библиотеки есть только пример и нет подробного описания как составляющие этот пример 10-15 строк взаимодействуют между собой?
А сегодня это если ещё не единственный вариант — то типичный.
Но как минимум у вас должен быть ответ почему вы вызываете этот метод.
Но как минимум у вас должен быть ответ почему вы вызываете этот метод.
Этого, к сожалению, недостаточно. Т.к. библиотечный метод может принимать довольно странные комбинации аргументов, давать достаточно неожиданные возвратные значения. Да еще и исключения кидать. И все это нужно держать в голове
А вот знать что ты написал — это надо требовать от всех без исключений.
Собственно это вообще первое что надо требовать от джуна.
Надо определиться.
О какой библиотеке речь?
О стандартной? Или о какой-то third-party, написанной джунами?
один раз прочитал и используешь и знаешь как работает
Все несколько сотен страниц?
Если не хватает — читаешь ту часть, которую надо и продолжаешь спокойно жить с этим знанием.
ссылка, где можно выбрать компилятор и его опции и посмотреть ассемблер
push 1
push 1
push OFFSET `string'
call _printf
И даже больше — некоторые убирают вызов F1 и делают inline кода прямо в main`e.
Да еще вспоминаем, чем отличается i++ от ++i и кажется все логично — результат 1 1.
Было бы интересно посмотреть на вопросы шарпистам. Не хотите "спалить" какой-нибудь красивый вопрос в следующей статьей? ))
Стандартный ответ «не знаю, и предполагать не буду
Так у них же другая специфика по сравнению с другими фирмами использующими C++.
У них С++ это не только язык программирования, но и предметная область.
У них С++ это не только язык программирования, но и предметная область
И что? Для диагностики вполне достаточно написать «вот здесь неопределённое поведение». Не требуется никакой приписки «и мы предполагаем, что поведёт оно себя вот так».
Не требуется никакой приписки «и мы предполагаем, что поведёт оно себя вот так»
Это конечно из раздела "мое воображение", но оно может потребоваться при общении с клиентом, который скажет ваш инструмент пишет что здесь у меня "UB", а у меня все работает, в таком случае знание что "если собрать компилятором X с опциями Y, то UB покажет себя во всей красе" может сэкономить кучу времени на объяснения что такое UB и почему это плохо.
Из личной практики, как-то на ревью кода я пытался убедить что стоит все-таки писать va_end иначе это противоречит стандарту, а мне в ответ тыкали тем, что вот с данным компилятором и на данной платформе va_end это пустой макрос.
Но вообще скорее всего это вопрос в том числе и способ "повернуть" разговор на
тему как работает компилятор и какие у современных компиляторов есть оптимизации
использующие undefined/unspecified behaviour. Ведь эти грабли с неопределенным
порядком вычисления аргументов положили не просто для того чтобы сделать жизнь
пользователей C++ поинтереснее.
Это конечно из раздела «мое воображение», но оно может потребоваться при общении с клиентом, который скажет ваш инструмент пишет что здесь у меня «UB», а у меня все работает
Да, фактор дебилов я не учёл. Но не понимаю, почему вопросы с дебилами должны решать программисты.
Но не понимаю, почему вопросы с дебилами должны решать программисты.
Ну опять же специфика проекта (опять же воображение, так как я никак не связан с PVS): в теории можно иметь и в отделе поддержки людей хорошо знающих и разбирающихся в C++, но это немного дорого по-моему делегировать таким людям только поддержку, рационально на мой взгляд в случае такой предметной области распределить поддержку пользователей среди программистов-добровольцев.
Юрий Минаев: Не связывайтесь с поддержкой C++ программистов.
- «Не знаю», это совсем не ответ.
- А вот UB — интереснее. Это можно пообсуждать? Почему UB? Где тут UB?
Но у вас, как уже заметили, своя специфика.
реквестирую от вас статью ТОП-10 вопросов на наших собеседованиях.
кстати, а у вас есть проверки на такой код:
memset(static_cast<void*>(&settings), 0, sizeof settings);
?
memset(static_cast<void*>(&settings), 0, sizeof settings);
Мало данный и непонятно что хочется найти. Что такое `settings`? Что вокруг `memset` в коде?
Да, данных мало. Объект создаётся, зануляется, заполняется и потом передаётся аргументов в функцию.
Меня зацепил просто тот факт, что в случае, когда всё хорошо со структурой, то данное явное приведение избыточно: memset()
принимает void*
, а любой указатель может к нему приводиться неявно.
Дальше оказалось, что это маскировка предупреждения компилятора о том, что структура нетривиальная. И действительно, у неё оказался конструктор, в котором происходит установка значения полей.
ЕМНИП, memset
и memcpy
на нетривиальные типы — это неопределённое поведение. Конкретно здесь, всё было и будет хорошо с точки зрения поведения с используемым компилятором (хотя я не могу себе представить, как тут можно напортачить или испортить жизнь разработчику и потребителю кода): нет виртуальных функций, прямым доступом инвариант не поломаешь и так далее.
Ну и возвращаясь к исходной конструкции, она избыточна, это раз, может замаскировать проблему, если класс достаточно сложный или вдруг появятся в нем виртуальные функции, это два.
И в дополнение, сама структура из сторонней библиотеки, конструктор там появляется под условной компиляцией, если сборка C++ компилятором. Примеры для библиотеки идут, в основном, для Си и там повсеместно memset
.
В общем, наличие явного приведения типа в memset
/memcpy
, хоть и не является ошибкой, но очень подозрительно.
ЗЫ Я предложил использовать {}
для инициализации, типа:
Foo settings{};
Хотя да, я встречал реализации memset
/memcpy
на контроллерах, что принимали uint8_t*
, но мне кажется, это уже другой пласт проблем.
struct SettingsA
{
int x;
SettingsA() : x(10) {}
};
struct SettingsB
{
int x;
SettingsB() : x(10) {}
virtual void V() {}
};
void Set()
{
SettingsA settings_a;
memset(static_cast<void*>(&settings_a), 0, sizeof settings_a); // тихо
SettingsB settings_b;
memset(static_cast<void*>(&settings_b), 0, sizeof settings_b); // V598
}
У PVS-Studio здесь вечный компромисс между дотошностью и шумностью. Он промолчит про случай SettingsA, хотя формально так делать нельзя. Почему? Потому, что в мире слишком много такого кода. И на практике он нормально работает. Я не говорю, что так нужно/можно писать. Однако, не хочется воевать с ветряными мельницами.
Если дело серьезней, например, есть указатель на таблицу виртуальных методов, то тут уже молчать никак нельзя. Вызов memset явно всё портит и анализатор выдаст:
V598 The 'memset' function is used to nullify the fields of 'SettingsB' class. Virtual table pointer will be damaged by this. test.cpp 39
P.S. И да, есть отважные программисты: Примеры ошибок, обнаруженных с помощью диагностики V598.
Да, я написал, что по факту ничего не ломается. Но интересна мотивация написания лишних букв. Хотя хорошо, что PVS разделяет два случая, по крайней мере, если уж написано, поможет не отстрелить ногу. Но сколько мест, где нет ещё статического анализа, а тут и компилятор помочь предупреждением может, а его затыкают.
Но сколько мест, где нет ещё статического анализа, а тут и компилятор помочь предупреждением может, а его затыкают.Ну пока из вашего описания невозможно понять ни в чём проблема, ни на что конкретно вы жалуетесь.
Дальше оказалось, что это маскировка предупреждения компилятора о том, что структура нетривиальная. И действительно, у неё оказался конструктор, в котором происходит установка значения полей.Что совершенно не запрещает использовать
memset
и не вызывает предупреждений у clang/gcc/icc/msvc.Возможно в вашем примере было что-то ещё?
Потому что желание использовать memset как раз понятно. Он эффективнее. Сравните bar и baz.
Хотя это «экономия на спичках», конечно.
Но, с другой стороны, если
memset
никогда не хуже и иногда лучше — то почему бы и нет?не вызывает предупреждений у clang/gcc/icc/msvc
я же не на ровном месте написал, что вызывает предупреждение компилятора:
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <type_traits>
using namespace std;
struct foo_t
{
int a;
foo_t()
{
a = 0;
}
};
int main()
{
foo_t settings;
memset(&settings, 0, sizeof(settings));
cout << std::is_trivial_v<foo_t> << '\n';
}
https://wandbox.org/permlink/1O3QXcXX4C3ZSOQt
Предупреждение:
prog.cc: In function 'int main()':
prog.cc:21:42: warning: 'void* memset(void*, int, size_t)' clearing an object of non-trivial type 'struct foo_t'; use assignment or value-initialization instead [-Wclass-memaccess]
21 | memset(&settings, 0, sizeof(settings));
| ^
prog.cc:9:8: note: 'struct foo_t' declared here
9 | struct foo_t
| ^~~~~
Потому что желание использовать memset как раз понятно. Он эффективнее.
Пока у структуры не появится инвариант, который можно поломать.
Я потому тему и поднял, потому как упоминание UB я встречал в https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-memset но там конкретно:
Using memcpy to copy a non-trivially copyable type has undefined behavior.
т.е. только про memcpy()
, хотя, положа руку на сердце, все аргументы, которые у меня в голове рождаются касательно memcpy()
, вполне себе применимы и для memset()
.
И да, там речь идёт о non-trivialy-copyable больше.
И таки в стандарте я пока не откопал нужной строчки, пока где-то рядом, да около. Сам GCC:
For example, the call to memset below is undefined because it modifies a non-trivial class object and is, therefore, diagnosed.
И пример кода:
std::string str = "abc";
memset (&str, 0, sizeof str);
Я пока ничего не утверждаю и просто пытаюсь для себя разобраться. В частности, почему при копировании может быть UB, а при memset — нет и всё штатно (ну пока оно без vtable, хотя тут и memcpy стреляет).
Продолжу монолог/рассуждения в слух :)
В общем, судя по всему:
- https://en.cppreference.com/w/cpp/string/byte/memset
- https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable
- https://en.cppreference.com/w/cpp/named_req/TrivialType
UB здесь действительно нет. И единственная потенциальная проблема — "переопределение" конструктора по умолчанию.
Плохо, что в GCC смешали предупреждение для тривиального типов и для trvially-copyable типов.
В вашем примере тип Settings соответствует критерию тривиальности — там конструктор по умолчанию автоматически сгенерированный. Добавьте static_assert(std::is_trivial<Settings>::value)
, компиляция пройдёт.
Если уберёте второй конструктор (он в коде не используется), то bar()
и baz()
перестанут отличаться:
А если переключиться на clang, то даже исходный пример перестанет отличаться:
Но ладно, всё равно мой случай более корректно описывает такой пример:
Да, memset()
тут выглядит эффективнее, до тех пор, пока конструктор инициализирует поля нулями. В противном случае поведение по умолчанию отличается от задуманного.
Ну далее, как я писал выше, после инициализации структуры происходит заполнение каждого поля. В структуре конструктор по-умолчанию — с аргументами (со значением по-умолчанию), так что код будет выглядеть как-то так:
И тут даже на GCC отличий нет.
Ещё мешает инвариант и когда в конструкторе происходит что-то вроде:
a = 22;
b = 34;
c = 127;
Повторюсь, конкретно в нашем случае, это не так, там нули, т.е. в реальности memset()
ничего не ломает, по крайней мере на x86_64 и GCC 9.2 (ну и я не вижу вообще каких-то причин там чему-то сломаться на других связках).
Ну мне он даже не совсем мешает. Мне просто любопытно. Немного истории: код давешний и изначально было просто:
memset(&settings, 0, sizeof settings);
Компилятор был GCC версии < 8.0. В версии 8.0 вводят -Wclass-memaccess и ключают его в -Wall
.
Происходит переезд на GCC 9.2. Появляется предупреждение. Его фиксят кодом приведённым выше, т.е. явным приведением. Тогда как куда логичнее (ну как логичнее, мне логичнее) было бы убрать memset()
и поставить {}
после объявления переменной settings
.
Т.е. как бы тут и не сломано ничего, и пусть оно так и будет. Но захотелось по-разбираться, а тут повод такой, спецам вопрос задать :)
Я допускаю, что наличие такого стандарта " по факту " с единственной актуальной версией компилятора лучше, чем наличие "стандарта" уровня международной организации типа ISO, но при этом каждый имплементирует как хочет с вытекающими девиациями, которые нужно отлавливать при смене компилятора. Сложный вопрос, в общем. А люди действительно пока не научились писать руководящие документы без взаимоисключающих параграфов или существенных дырок с отдельными случаями
Без ISO был бы вообще невообразимый пипец, не просто какие-то там «девиации», а вообще какие-нибудь несовместимые реализации, вплоть до того, что в одной реализации некое ключевое слово вполне могло бы означать одно, а в другой — совершенно другое.
Эм… Как бы и с ISO это не помогает — реверанс в сторону compiler-specific options. Я просто очень долго развлекался сведением к общему знаменателю кода между gcc/msvc/watcom. И, да, это не совсем про С++, но и про него тоже.
А потом будет все то же самое
скорее соглашусь.
program Hello(input, output);
var
s: string[10];
begin
s := 'AA';
s := s + 'BB';
writeln(s);
end.
Все компилируется, работает, печатает «AABB», все хорошо. А теперь ставим в начале директиву "{$mode iso}", и начинается секс… :)
Без ISO был бы вообще невообразимый пипец, не просто какие-то там «девиации», а вообще какие-нибудь несовместимые реализации, вплоть до того, что в одной реализации некое ключевое слово вполне могло бы означать одно, а в другой — совершенно другое.Собственно в качества примера могу рекомендовать глянуть на Pascal. Лет 20-25 он был зело популярен, даже некоторые весьма популярные операционки на нём писались изначально… а потом — кончился.
Во многом как раз потому что разные реализации не позволяли обмениваться исходниками без соврешенно диких напрягов…
Да, весело было.
Но принципиально было направление движения: после выхода стандарта C++98 разработчики большинства компиляторов взяли стандарт и начали его реализовывать.
Вот в те времена, о которых вы пишите я помню даже тест у нас был и там был вопрос: «сколько C++98-совместимых компиляторов C++ вы знаете». Как я потом выяснил за каждый названный компилятор снимался один балл…
А потом… всё начало «сходиться к стандарту».
Да, процесс занял порядка 10 лет, но ближе к концу нулевых всякие нестандартные фичи стали считаться «дурным тоном».
А вот Pascal — там всё было наоборот: каждый разработчик считал своим долгом расширить всё особым, несовместимым ни с чем способом.
При это «улучшениям» подвергались как раз самые базовые возможности: указатели на функции, работа с файлами, строками… много вы программ, которым ничего этого не нужно видели?
Всё упёрлось, по большому счёту, в то, что каждый разработчик надеялся сам «окучить» всю поляну и срубить бабла со всех разработчиков. Потому что ведь у них же не будет выбора: они завяжутся на его фичи и будут «по гроб» за них платить!
В результате — просто тупо всех разработчиков растеряли.
А ещё, кстати, у такого языка, как Ada, был и есть стандарт, тоже уровня ISO.Дык у Pascal было их аж два. И даже есть такие экзотические вещи, как ECMA-372.
А у Java и Python как раз ISO-стандартов нету.
то есть, каких-то принципиальных отличий де факто от того же паскаля не было.Было. Но вот, в качестве примера, древний, как говно мамонта, DR-DOS. Читаем:
The following third party tools were used to build the executables. Other versions of these tools may work but have not been tested.То есть даже тогда, когда упоминаются строго конкретные версии каких-то компиляторов — часто упоминаются два, три, а иногда и больше.
Tool Component
==== =========
Watcom C v7.0 COMMAND
Borland C v2.0 COMMAND
Microsoft MASM v4.0 IBMBIO, COMMAND
Microsoft Link v5.10 IBMBIO, COMMAND
Microsoft Lib v3.0 IBMBIO
Много вы подобного видели в приложении к Pascal или Delphi? Наоборот, скорее вспоминаются фразы «работает только с Turbo Pascal 3» или «Совместимо с Delphi 7, но не Delphi 8».
И, насколько я сейчас могу судить, в ранних нулевых совместимость между компиляторами мало кого волновалаАж настолько никого не волновала, что куча библиотек поддерживала 2-3, а то и больше компиляторов? Так никому не нужно было, что были созданы Metaconfig и Autoconf?
Нет, какая-то часть участников экосистемы стремилась-таки «перетянуть одеяло на себя» (те же Borland или Watcom)… ну и кончили они примерно там же, где и Pascal (последняя релизнутая версия Watcom, OpenWatcom 1.9 — это 2010й и версия 2, которая всё никак не выйдет, даже не поддерживает C++11).
Но были и другие силы. Был проект GNU и GCC, были имитировавшие их ICC и PGI (правда Intel пытался сразу усидеть на двух стульях имитируя под Windows MSVC, а под Linux GCC).
То есть было достаточно центростремительных сил для того, чтобы язык сохранялся единым. Причём это как раз в нулевые произошло: разработчиков GCC (так-то уже достаточно популярным) даже не спрашивали в принципе во время разработки C++98, а когда разрабатывали C++11 — то было уже понятно, что если что-то не будет поддержано GCC… то этого чего-то, можно считать, в стандарте и нету.
В общем можно долго обсуждать почему разработчики C++ стремились к чему-то единому, а разработчики Pascal или, там, Basic — не стремились… но результат — нагляден и очевиден.
А как раз формальный штамп ISO ничего не даёт: у Pascal их аж целых два — а толку?
наличие конструктора по умолчанию никак не влияет на свойство trivially copyable
Так собственно этот момент меня и поставил в ступор. Ругань именно на non-Trivial тип, который при этом остаётся trivially copyable (TrivialType = TriviallyCopyable && нет-пользовательских-конструкторов-по-умолчанию). И именно memset.
Я вот подумал, вполне можно было бы написать приведение явное, если это вызвано вопросами производительности (на самом деле нет), а перед поставить static_assert(std::is_trivially_copyable_v<Settings>);
. Тогда если вдруг в будущем класс станет ещё и non-TriviallyCopyable это свалится на этапе компиляции и защитит тылы. Сейчас же создана ловушка, которая может сработать, а может и не сработать в будущем.
#include <iostream>
struct Trivial {
int m;
Trivial(int a): m(a) {}
};
struct NonTrivial {
int m;
NonTrivial(int a): m(a) {}
virtual ~NonTrivial() {}
};
int main()
{
std::cout << std::is_trivially_copyable_v<Trivial> << " "
<< std::is_trivially_copyable_v<NonTrivial>;
}
выведет «1 0». Хотя gcc -Wall выдает предупреждение в случае попытки применить memset к переменной типа Trivial (а clang, кстати, нет — он предупреждает только в случае NonTrivial), но в реальности делать memset trivially copyable типам разрешено.
std::is_trivial / std::is_trivial_v выводит 0: https://ideone.com/FCHQwc
В данном случае GCC ругается именно на non-trivial тип:
```:13:39: warning: 'void* memset(void*, int, size_t)' clearing an object of non-trivial type 'struct Settings'; use assignment or value-initialization instead [-Wclass-memaccess]
потому я и предположил, что проверка должна быть соответствующей.
Неопределённого поведения в нём нет, если виртуальные функции не используются, и если мы работаем с нормальными платфомами, где
bool
, nullptr
и +0.0
состоят спрошь из битов со значением 0 — то в чём, собственно, подвох?www.viva64.com/en/w/v597
если мы работаем с нормальными платфомами, где bool, nullptr и +0.0 состоят спрошь из битов со значением 0
Вообще-то, всё наоборот: перечисленные платформы называют «нормальными» лишь потому, что написано огромное количество таких memset'ов. А потом появляются люди, которые с удивлением узнают, что null pointer и целочисленный ноль — разные вещи. А ведь нормально — это не писать такой код.
ну должны же быть границы у UB (не прям совсем всегда, и у любого, а вот в этом конкретном случае)
Очевидно, что 1, 2 в любых комбинациях может вывести.
А вот если вдруг выведет 0 или 3, да ещё и так, что собеседуемый это обоснует и докажет, а также продемонстрирует — вот тогда да, повод уже почесать репу...
ну должны же быть границы у UB (не прям совсем всегда, и у любого, а вот в этом конкретном случае)Нету. Вот совсем нету. Стандарт это подчёркивает отдельно:
If any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation)Фраза в скобочках — это не моя приписка, она в стандарте.
А вот если вдруг выведет 0 или 3, да ещё и так, что собеседуемый это обоснует и докажет, а также продемонстрирует — вот тогда да, повод уже почесать репу...Ну откуда можется взяться 3 — неизвестно, а вот выкинуть весь код функции — можно было бы вполне.
хм, а разве пример с printf не UB до C++17?
1) If a side effect on a scalar object is unsequenced relative to another side effect on the same scalar object, the behavior is undefined.
на cppreference даже пример приведен:
f(++i, ++i); // undefined behavior until C++17, unspecified after C++17
забавно, что разработчики PVS Studio это не читали :))
Действительно, и как это я не заметил. Спасибо, товарищ капитан
И там, и там изменяется одна переменная между двумя точками следования.
i++ — возвращает текущее значение, а в переменную записывает новое
int i=1;
f(i++); //функция вызовется с параметром 1
int i=1;
f(++i); //функция вызовется с параметром 2
Плохо написано.
!== выглядит, как будто это реальная операция, а не "два пути". А в этом случае может сперва выполниться правая часть. Или левая. И сделать неравенство совершенно очевидным.
забавно, что разработчики PVS Studio это не читали
Ещё забавнее, что никто из кандидатов не читал, и все вместе они уверены, что знают ответ :)
С определенного момента, если программа на C++ делает нечто странное — для очистки совести приходится смотреть, что выдает gcc -S
А вариант выучить язык, на котором пишете, вы совсем не рассматриваете? Ну, тогда попробуйте санитайзеры.
Я воспитан еще в той идеологии, что программисту виднее где оптимизировать, а где — нет. И да, я знаю что этот подход несовместим с шаблонами и автоматической кодогенерацией. Тем хуже для шаблонов.
Только вот компиляторы развиваются как раз в сторону оптимизации кода, и тем хуже как раз для вас, а не для шаблонов :)
В итоге, C/C++ остался для программирования ближе к железу (контроллеры). Но там удается оставаться в рамках разумного подмножества языка.
Мы пришли к выводу, что рабочее время с большей пользой можно употребить на решение актуальных задач, а не на вспоминание местами странного синтаксиса шаблонов, угадывание причин ошибки из простыни вывода gcc на два экрана, или перекомпиляцию 90% кода проекта из-за незначительных изменений в одном файле.
Это не значит, что мы совсем не используем шаблоны по причине религиозного неприятия. Скорее, мы их используем в духе раннего Страуструпа для избавления от написания дублирующего кода (то есть генерации нескольких версий класса для разных входящих типов). А вот мета-программирование, шаблонная магия, бусты — извините, но нет.
В свое время, посмотрев в какую сторону поехал C++, мы для больших систем перешли на Java. Хотя по #define, typedef и RAII скучали очень.
Отлично! Всегда радуюсь, когда в среде плюсовиков становится меньше любителей #define
А если без шуток, то опять-таки понятны ограничения #define — ну так и не нужно пытаться делать с ним clever tricks.
#deinfe IR_PIN_HIGH ((PIND & (1<<4))!=0)
...
if(IR_PIN_HIGH) { foobar(); }
Читаемость лучше? Однозначно! Дублирования кода меньше? Меньше! Почему это не применять? Ну да, понятно что можно сделать inline-функцию для того же. Но inline — это же пожелание, а не обязанность компилятора. А в контроллерах бывают места, где надо что-то проверить или дернуть биты в регистрах за определенное число тактов. Что касается этого define — компилятору трудно что-то неправильное с ним сделать.
Вторая супер-способность #define — это обращать куски кода в no-op.
#ifdef DEBUG
#define DEBUG_ONLY(x) x
#else
#define DEBUG_ONLY(x)
#endif
...
DEBUG_ONLY(red_led_toggle());
Ну да, синтаксис не очень… Но если мы компилируем без отладки — вызова red_led_toggle нет в принципе — и никаких проверок в этом месте тоже!
Для сравнения — в Java, даже в лучших реализациях отладочных библиотек (типа Log4j) проверка включения отладки делается в рантайме, и отладочный код всегда присутствует. Невозможно сделать его no-op на этапе компиляции (но там это обычно и не критично).
Жаль вас разочаровывать, но это не так. И тем не менее, даже при том, что иногда попадаются случаи, когда не избежать использования препроцессора, любить тупорылую текстовую подстановку — это какой-то особый изврат. Я и действительно радуюсь, когда любители оного сваливают в другой язык.
Та же текстовая подстановка — это начиная от search/replace в редакторе, через sed и до регулярных выражений (которые теперь есть вообще везде). Может быть она тупорылая — но простая, интуитивно понятная
Ага, именно поэтому говорят «я стал решать проблему с помощью регулярных выражений — теперь у меня две проблемы». Это как парадокс блаба — если вы не использовали синтаксических макросов (в каких-то других языках) и/или не цените статическую типизацию, предоставляемую шаблонами, то мне не объяснить вам чем так плохи макросы. Боюсь, что даже если они ударят вас по лбу, как неаккуратно оставленные грабли (или пониже, если детские), то вы всё равно не поймёте проблемы, потому что не видите альтернативы.
Я пользуюсь препроцессором C, ну где-то с 90-91 года. Уж тридцать лет скоро! Но не помню я каких-то проблем, которые бы это вызывало. И у знакомых тоже нет.
Всё в этой жизни имеет недостатки, но вы их не видите уже в течение 30 лет. Вы хоть отдаёте себе отчёт насколько ваши слова подтверждают парадокс блаба?
Но еще бы сделал два замечания. Во-первых, нужно оценить сложность обработчиков. Накладные расходы на обработку чанка будут как минимум — выбор адреса виртуальной функции и 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-чанков.
А-а, да — ни препроцессор ни шаблоны мне лично тут не нужны. От слова «совсем».
Поскольку мы не делаем на этапе дизайна никаких предположений относительно внутреннего устройства InputProcessor — есть полная свобода для оптимизаций. Им не возбраняется иметь свое внутреннее состояние. Если обработчику известно, что на данном процессоре эффективнее всего обрабатывать чанки по 64 байта — пусть большие чанки обрабатывает in-place а мелкие — складирует себе в буфер и обрабатывает когда наберется 64 штуки (условно). Более того, никто не запрещает иметь один и тот же обработчик оптимизированный под разные архитектуры — и подменять его, например выбором динамической библиотеки на этапе загрузки приложения…
Но идея в принципе понятна, и я еще раз повторюсь — религия использовать сложные шаблоны нам не запрещает. Но прежде — смотрим другие варианты, и (как пишут в аннотациях лекарств) «применять, соизмеряя риски и ожидаемую пользу»… Как-то так.
Кстати, внутри вчера обсуждали то что я пишу — родилась альтернативная гипотеза. Мы ж пишем то на Java, то на C/CPP. Чтобы каждый раз не переключать мозг — оно само как-то построилось более-менее общее подмножество языков. Ну потому что #define boolean bool я в хедерах видел — видимо кого-то достало… :-)
Не, ну у меня в коде process_chunk, а не process_char.То есть вы, фактически, весь wc засунули в одну функцию и предложили эту функцию реализовать 15 раз? Гениально, да.
С таким подходом вообще ничего не нужно, если вы способны порождать десятки копий кода и нигде не ошибиться после copy-paste.
Мы ж тут типа все умные и понимаем, что и доставание данных откуда-то (файл, БД, network) происходит кусками, и обрабатывать их, стало быть, тоже посимвольно не следует.Вообще-то самое простое — это сделать
mmap
, получить один длинный кусок и его обработать.Но прежде — смотрим другие варианты, и (как пишут в аннотациях лекарств) «применять, соизмеряя риски и ожидаемую пользу»… Как-то так.Да именно как-то так:
$ time wc /lib/x86_64-linux-gnu/libc.so.6 5541 34419 1820104 /lib/x86_64-linux-gnu/libc.so.6 user 0m0.137s $ time LC_ALL=C wc /lib/x86_64-linux-gnu/libc.so.6 5541 34372 1820104 /lib/x86_64-linux-gnu/libc.so.6 user 0m0.033sКак раз coreutils сделаны по вашим заветам — и дико тормозят. Wc и grep, в частности…
И именно за счёт ваших любимых виртуальностей…
Интересно посмотреть, сохраняется ли она если дать на вход корректный UTF8-файл.Сохраняется. LC_ALL=C — это такой себе «админский» совет для работы с логами.
А там вообще обычно внутри чистый ASCII.
Ну тогда проблема, скорее всего, не в алгоритме расчета как таковом, а в преобразовании многобайтовых символов в wide-chars.Именно так. Поскольку писали они на C, то для многобайтовых кодировок у них как раз
callback
. А униформные по таблице обрабатываются.КПД хуже паровоза, нафиг…Почему нафиг? Если вы заинлайните обработку, то всё сведётся к банальной, хорошо предсказуемой, проверке
c < 0
. Скорость будет почти как у ASCII.А вот как раз «А-а, да — ни препроцессор ни шаблоны мне лично тут не нужны. От слова «совсем»» — ровно и привело к наблюдаемому эффекту.
Но что-то мне кажется, что преобразование UTF-8 — wide char сложно заинлайнить.Вам не нужно эту функцию инлайнить. Все символы, на которые вы должны реагировать — вообще однобайтовые. И всё что вам нужно — функция
charlen
. Она, для UTF-8, тривиальна и вырождается в несколько довольно-таки хорошо предсказуемых ветвления.Но вот её вызов — «стоит» изрядно.
Для единственной реализации часто лучше посмотреть на pimpl и не бросаться в абстракции и виртуальность
Ну это само по себе своего рода КОНТРАКТ.
Вы знаете, что какие-то части кода являются макросами, какие-то функциями. Постоянно (30 лет!) держите эти особенности в голове.
Завтра придёт ничего не соображающий джун и запердолит "вызов" макроса с параметром, меняющим стейт. Типа, FOO(C++). И оп-па, всё сломалось, успешной отладки!
Всё же с изначально константными условиями шаблоны обычно справляются не хуже макросов. Но в плюс к этому, смогут и неконстантные условия либо безболезненно переварить, либо выдать ошибку на 5 экранов (это много, но обычно лучше, чем отладочная сессия, покуда займёт всего час, а не полнедели)
Но inline — это же пожелание, а не обязанность компилятора.Чтобы это стало обязанностью есть __forceinline в MSVC и always_inline в clang/icc/gcc.
Что касается этого define — компилятору трудно что-то неправильное с ним сделать.Да легко. Вставить сдвиг на 4 прямо в код, например. Или вообще вызвать функцию из библиотеки. Почему вы так уверены, что этого не случится?
Но если мы компилируем без отладки — вызова red_led_toggle нет в принципе — и никаких проверок в этом месте тоже!Я вас умоляю.
if constexpr
прекрасно с этим справляются.#define в другом сильны.
Вот такой простейший #define, никакой шаблонной магией до сих пор не заменить:
#define printintvar(x) printf("\"%s"=%d\n", #x, x);
А вот вот это всё, что «нелюбители шаблонов» устраивают только делают код менее читабельным и сложнее в отладке.
Ну и уж тогда к трюку с printf еще бы добавить наличие predefined-определений __FILE__ и __LINE__. Получается автоматическая идентификация точки печати. Что в Java, так её разэтак, можно сделать только в рантайме через раскрутку стека, и это может быть запрещено полиси java-машины, и руководство предупреждает нас, что это может быть дорогая операция. А в C/CPP — бесплатно и на этапе компиляции!
Ну вот, начинаются compiler-specific keywords… :-( Некрасиво это по-моему.
Староверам сложно с этим смириться, но современные компиляторы лучше программиста знают как оптимизировать код. Нужно лишь им помочь — с помощью опций и корректного кода.
Ну и уж тогда к трюку с printf еще бы добавить наличие predefined-определений __FILE__ и __LINE__. Получается автоматическая идентификация точки печати.
Слава богу, скоро и этот рудимен можно будет выкинуть на помойку. Но те, кто считает, что «плюсы развиваются куда-то не туда», так и будут ворчать, что мир вокруг, оказывается, меняется.
К большому счастью, C++ старается сохранять совместимость с предыдущими версиями стандарта (и даже с C, где это возможно).
мне кажется, что это и является основной проблемой С++.....
Это дает возможность выбрать разумное подмножество языка (которое сложно чем-то испортить) — и на нем жить без особых проблем.
Проблема в том, что у разных библиотек это подмножество разное.
Я уж не говорю о примере khim выше, когда memset
эффективнее простой инициализации.
Ну там, всё-таки, был компилятор, который немного подотстал от «переднего края» (gcc сейчас очень мало кто занимается, все ушли на clang, по понятным причинам).Но вообще векторизация — это боль. Я думаю ещё лет 10 компиляторы не будут уметь прилично код векторизовать.
А научатся — так же, как и со скалярным кодом — когда не будет уж слишком большого количества требований к этому коду.
Приличный 16-битный код, кстати, ни один компилятор до сих пор не умеет генерить — уж очень там вычурно всё (в память могут ходит 4 регистра из 8 и при этом с дикими ограничениями и прочее).
Но вообще векторизация — это боль. Я думаю ещё лет 10 компиляторы не будут уметь прилично код векторизовать.
Я для этого экспериментировал с Интеловским компилятор и был еще такой забавный VectorC. Не знаю сдохли ли они и сколько они сейчас стоят для коммерческого применения. Да и вопрос по поддержке стандартов.
P.S. плачу по трупу Watcom'а
P.S. плачу по трупу Watcom'аНу вроде как Watcom ещё, теоретически, существует… но оптимизируе он хуже современных clang'а и gcc.
Когда-то да, он был на коне.
Я для этого экспериментировал с Интеловским компилятор и был еще такой забавный VectorC.Основная проблема с векторизацией в современном мире — это то, что мы вернулись куда-то в 80е. Когда операции с аккумулятором занимали 1 такт, а с любим другим регистров — 2.
И вот под это никогда не было качественных оптимизаторов. И сейчас нет.
То есть через 10 лет проблемы с векторизацией станут менее актуальны не за счёт прогресса компиляторов, а за счёт того, что процессоры станут более ортогональными. И вам не нужно будет думать какую из десяти инструкций пересылки из регистра в регистр в данном месте использовать.
Это не значит, что мы совсем не используем шаблоны по причине религиозного неприятия. Скорее, мы их используем в духе раннего Страуструпа для избавления от написания дублирующего кода (то есть генерации нескольких версий класса для разных входящих типов). А вот мета-программирование, шаблонная магия, бусты — извините, но нет.Если вы вообще, в принципе, можете их использовать — то это значит, что вы работаете с компилятором, способным вот все эти неочевидные опимизации проделывать.
То есть вы всё равно платите за них — но не используете.
Странное поведение, как по мне. Всё равно что купить мощный спортивный автомобиль и ездить на первой передаче всё время. А то «как бы чего не вышло».
Продолжая аналогию с автомобилем — от того, что мне дали спортивный автомобиль, мне что теперь дрифтовать и гонять 180 по городу? Ну есть любители, да… А наша цель — скучно и предсказуемо добраться из точки «А» в точку «Б» максимально экономным способом. И да, многие возможности, заложенные конструкторами в машину за те же деньги (бесплатно) при этом остаются не использованными. Ну так это проблема конструкторов, а не моя…
С другой стороны, я видел открытые проекты с последними новинками C++. Для компиляции «с нуля» в пору ставить ферму. Потому что все эти фокусы с шаблонами память жрут как не в себя и время компиляции растет просто удивительно! Но у людей свой путь — надеюсь, они видят в нем какие-то выгоды, которые бы это все оправдывали.
А наша цель — скучно и предсказуемо добраться из точки «А» в точку «Б» максимально экономным способом.Ага. Конечно.
Почему тогда предложение обновить компилятор встречает такой яростный отпор, если у вас всё «скучно и предсказуемо»?
Рутинная операция, которая у нас случается раз в пару месяцев примерно.
Потому что все эти фокусы с шаблонами память жрут как не в себя и время компиляции растет просто удивительно!Да, есть такое дело. Но у вас, в общем-то, просто нет выбора: вы просто не сможете поддоживать сколько-нибудь нетривиальное количество кода с вамиши «скучными и предсказуемыми» #define'ами.
А на тех объёмах, где вы можете без шаблонов справитесь (то есть до миллиона строк) — вам ферма в любом случае не нужна будет…
Почему тогда предложение обновить компилятор встречает такой яростный отпор, если у вас всё «скучно и предсказуемо»?
Во-во-во, у этих любителей «разумного подмножества C++» код как правило такой разумный, что ведёт свою собственную жизнь, и потому обновление компилятора приводит к вылезанию всевозможных мест неопределённого поведения. Авторы это знают и всеми силами сопротивляются обновлению компилятора, рассказывая ужасные истории о том, что в нём багов больше, чем мир когда либо видел до этого. Сколько же я этого натерпелся…
И я не говорил, что мы не обновляем компилятор. Наш стандарт с 2008 года Debian Stable с тем компилятором, который там есть. Моя изначальная мысль была, напомню, что при старом компиляторе странное поведение программы было почти 100% результатом ошибки программиста (как правило, в управлении памятью). Сейчас для очистки совести стоит посмотреть, что выдает gcc -S. Язык стал сложный, кодогенерация стала сложной, рамки в которых компиляторам допустимо интерпретировать намерения программиста стали сложными… И меня тут не сильно волнует, кто виноват — дизайнеры языка, авторы компилятора или программист. Очевидно, что программист средней квалификации не в состоянии прочитать и держать в памяти текущий стандарт C++ со всеми его тонкостями. Но работать и решать задачи при этом все-равно нужно. Вывод gcc -S позволяет понять, как именно компилятор понял ваши намерения. Дальше можно искать в стандарте, почему он это сделал и какими словами его направить в более правильную сторону.
Вывод gcc -S позволяет понять, как именно компилятор понял ваши намерения.Да. Данная, конкретная версия компилятора в данный конкретный момент времени с данными, конкретными ключами компиляции.
Прямой путь к написанию кода, который от малейшего «шевеления» развалится.
Дальше можно искать в стандарте, почему он это сделал и какими словами его направить в более правильную сторону.Но будет ли «программист средний квалификации» это делать? Вот ведь в чём вопрос.
То что для программиста высокой квалификации взгляд на то, что разные компиляторы генерируют для той или иной конструкции может помочь сделать выбор — очевидно. Но вот как раз «средний» программист, о котором вы так печётесь склонен работать совсем по другому — наплевав на стандарт и руководствуясь подходом: «я посмотрел, что оно работает — и хорошо».
Ну а чем это дальше кончается — всем известно.
github.com/maxim-kuvyrkov/ninja/commit/4fb7ab82334efcc5cf4fc45664a632a8d9ff0813
Есть и другие патчи которые ограничивают запуск задач в зависимости от потребления памяти (как это делается с CPU например в мастере)
ограничивало бы компиляцию не по числу задач, а по потреблению памяти.
а это заранее просчитать можно-то? У меня боль и окончание памяти на 8 потоках недавно только сборка QWebEngine вызвала (точнее части с хромиумом), тоже захотелось ограничение по памяти. Я к тому, что начал строить юнит, а его как начало пучить...
По поводу 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 на шаблонах. Плохо можно на чём угодно написать.
Естественно сначала отладиться пришлось. Не без этого. С сообщениями об ошибках тоже особых проблем не было.
генерировать С (или LLVM IR) руками
А вы уверены, что в вашем генераторе не будет ошибки, которая приведет к генерации кода с UB, хехе?
if(a && b && c) {
foobar1();
} else if (a && b && d) {
foobar2();
} else if...
Скучно, немолодежно, без шаблонов — но железобетонно и никаких UB. :-)
Тут и ответ на вопрос, почему многое продолжают писать на чистом Си.
Но и на чистом Си тоже вполне пишется, если не надо особых абстракций — а просто жесткую логику реализовать. Как подумаешь, сколько под это надо было раньше корпусов 74 (155) серии поставить… :-)
А вот cin-cout не зашли
Надо же, не только мне не понравилось… Хотя вот std::format облегчит жизнь (а boost::format для читеров).
Собственно я писал, повторюсь, не о том, что Си лучше С++ (это слишком толсто тут), а о том, что схема «DSL -> C» популярна не просто так, и интересно было услышать это и от 0xd34df00d. И он конечно прав, что лучше сразу LLVM IR, но сей байткод сложноват, если надо вдруг «руками полазить», тут уж лучше Си — один из самых простых языков все-таки.
И он конечно прав, что лучше сразу LLVM IR, но сей байткод сложноват, если надо вдруг «руками полазить», тут уж лучше Си — один из самых простых языков все-таки.Вот только пресловутых UB — в нём не сильно меньше, чем в C++.
поэтому говорить, что С однозначно лучше, я бы не стал
Упаси Боже, этого и я не говорил.
Плюсы на пару порядков сложнее, но в плюсах есть ряд возможностей сложность этого рода как-то спрятать
В целом да, но тут либо нужен талантливый прятатель в команде, либо надо строго не выходить за рамки примитивного сабсета, если у команды есть ограничения.
Конечно, просто, на мой взгляд, выбор С это подразумевает
Мы с вами уже это обсуждали, да и без обсуждений очевидно — выбор Си подразумевает одновременно незнание более сложных языков, отсутствие необходимости писать реально большие программы и наличие потенциальной необходимости устраивать ад (в хорошем смысле этого слова). Поэтому так много Си в эмбеде — ад часто нужен, программы небольшие, а на полноценное изучение плюсов у эмбедера (который, особенно в России, делает еще сто дел одновременно) просто не остается времени. Вряд ли кто-то при этом видит реальные плюсы от использования именно чистого Си, ибо кто бы отказался от неймспейсов например? Да никто.
отсутствие необходимости писать реально большие программы
это не очень сильный аргумент. Вот взять то же ядро линукса — это большая программа? Мое мнение — огромная.
C++ начинает играть именно там, когда начинает не хватать возможности описывать сложные взаимосвязи между сущностями — все-таки ООП на коленках в С — это мрак. С другой стороны, я помню плюсы временем 2003-го с MFC… Какой-либо подстраховки разработчика от выстрела в ногу нет. А с плюсами как более мощным инструментом легко себе не просто выстрелить в колено, а оторвать обе ноги
незнание более сложных языков
перефразируя известную поговорку — лучше быть полиглотом и хорошим человеком, чем негодяем и знать только один язык
это не очень сильный аргумент. Вот взять то же ядро линукса — это большая программа? Мое мнение — огромная.
Ну вы же понимаете, что имелось в виду не «большая, много строк», а «большая, сложные взаимосвязи». И, да, ядро Линукса в пример, но во-первых там выбора не было, а во-вторых это скорее исключение из общего правила.
Вот взять то же ядро линукса — это большая программа? Мое мнение — огромная.Во-первых, по современным меркам, она уже не слишком большая, а во-вторых они там очень сильно выходят за рамки C и используют чуть не все расширения, которые были, в принципе, придуманы разработчиками GCC.
Именно потому что им отчаянно не хватает вещей, которые в C++ есть в стандарте.
просто привел для сравнения размера проектов
Kubernetes — 2MLOC
CockroachDB — 0.5MLOC
etcd — 441KLOC
Почему-то не замахнулись писать на С++… Хотя казалось бы — сетевые приложения должны быть классическим примером использования плюсов.
Очень интересно сравнить с другими большими проектами на С++ и оценить — действительно ли массово пишут много кода на плюсах или это очередной миф.
p.s. варианты — давайте посчитаем исходные коды windows нечестны априори — т.к. там 100500 компонентов и сборная солянка из разных языков — начиная от С и кончая вроде даже паскалем
p.s. варианты — давайте посчитаем исходные коды windows нечестны априори — т.к. там 100500 компонентов и сборная солянка из разных языков — начиная от С и кончая вроде даже паскалеНу да, разумеется. А давайте ещё исключим все проекты, написанные более, чем одним человеком — и получим вообще нирванну.
Крупные проекты — это всегда «сборная солянка». В том же ядре Linux есть и python и много чего ещё, по мелочи.
И да, они зачастую состоят из отдельных компонент — Android, Chromium, даже какой нибудь Photoshop будут содержать в себе десятки компонент. Что в этом странного-то?
Хотя казалось бы — сетевые приложения должны быть классическим примером использования плюсов.Во-первых их на плюсах… хватает. Во-вторых как раз под сетевые приложения Go ложится идеально: не нужно общаться со сторонними библиотеками, не нужен GUI (что, в сущности, вариант предыдущего пункта) и очень важна поддержка многопоточности. И то — максимум, что вы накопали — это 2 миллиона строк кода. Это, как бы, размер 3D-движка Unreal Engine (даже не всей игры).
проблема в том, что дальше надо будет смотреть, что делает clang -S. А ещё дальше — cl.exe -S (или что там). Тогда приходит понимание, что дешевле просто устранить причину.
Правильный ответ на любой подобный вопрос: "давайте сначала посмотрим, что скажет компилятор".
А компилятор скажет следующее:
prog.cc: In function 'void F1()':
prog.cc:8:28: warning: operation on 'i' may be undefined [-Wsequence-point]
8 | printf("%d, %d\n", i++, i++);
| ~^~
prog.cc:8:28: warning: operation on 'i' may be undefined [-Wsequence-point]
Вот и весь разговор.
Потому как эти ребята как раз и занимаются тем, что учат компилятор ругаться на разные проблемные места и если они будут решать что и где должно происходить, опираясь на компилятор… в общем ничего особо хорошего из этого не выйдет.
Я, как человек, знающий C, утверждаю, что этот код печатает полный текст доказательства, что N==NP, и все остальные любопытные вещи, скрытые в гримуаре с названием UB.
Хотел бы коснуться различия поведения gcc и msvs.
Хотелось бы понять почему по разному реализовали порядок передачи парметров в функцию. У мсвс по умолчанию используется порядок fastcall?
Кстати, использование такого кода может служить иллюстрацией того в каком порядке передаются параметры функций.
Я понимаю, что всё это ub. Но Аби никто не отменял.
godbolt.org/z/HLNGy3
Должно распечататься: 1, 1. Все остальное — от лукавого ;)
Поясню.
Функция prinf на момент запуска имеет текущее значение i = 1. С этим значением в качестве параметров она и запускается. Далее (да хоть одновременно с ней) запускаются два оператора i++. Здесь, конечно, явная проблема — два оператора получают одновременный доступ к одной переменной. Это и есть существенная проблема данного кода. Но поскольку в обычной ситуации они запускаются последовательно, то пренебрегаем этой проблемой. Получаем в итоге i = 2;
Вот и все ;).
Думать надо «ширее».
Компилятор — это обычный калькулятор. А все калькуляторы должны работать одинаково. «Забавно» другое: наблюдать обсуждение, что правильнее — 2*2 равно 3 или 5, обсуждая работу только «калькуляторов», но забывая про… арифметику.
Т.е. сначала все же нужно разобраться сколько же будет 2*2 на самом деле…
Поэтому, думаю, соискателей нужно тестировать на знание арифметики, а не на способность шустро давить на клавиши всех типов «калькуляторов». Хотя, не спорю, для кого-то важнее, возможно, именно последнее. Речь об этом. ;)
Арифметика… в школе делить на 0 нельзя, а в универе — можно. В школе sqrt(-1) нельзя, а теории комплексных чисел это мнимая единица.
Арифметика от условий тоже может отличаться. На какой-то платформе эффективнее одно, на другой — другое. На одной платформе поведение было реализовано очень давно и много чего на него завязалось уже, на другой сделали эффективнее, но спустя годы.
От многих знаний — много печали.Нужно подумать, прежде чем лезть на вышку, когда речь идет о простой арифметике, — есть риск сорваться.
Но мысль интересная — поставить в зависимость от платформы уже и работу калькулятора ;)
Но все же, как представляется, нужно видеть берега. Иначе…

Понимаю, что современные компиляторы работают эффективней, но вот так.
Понимаю, что современные компиляторы работают эффективней, но вот так.А вот как именно? Почему язык должен вести себя неестественным образом для той архитектуры, на которой он был изначально сделан и также неестественным образом для существующих компиляторов?
Почему ориентиром должно быть что-то, что, если и является, в каком-то смысле, естественным, но не возникает само собой никогда, кроме краткого периода где-то в начале 80х (напоминаю что сегодня на дворе 2020е, а создали C изначально в 1970е… на PDP-11)?
Глубина кроличьей норы или собеседование по C++ в компании PVS-Studio