Комментарии 80
А у этой проблемы с union на С++ есть стандартное zero-cost решение?
В С++ специально для реинтерпретации есть std::bit_cast
.
Так и в С никто не хранит данные в юнионах долгосрочно именно потому, что может вдруг понадобиться реинтерпретация. Такой юнион возникает на сцене кратковременно, именно для этой цели. И исходные данные в него приходится загружать, а переинтерпретированный результат - выгружать.
всмысле не хранит? hello from каждая первая реализация json, yml, msgpack, он там конечно не используется для кастов но говорить что данные в union долгосрочно не хранят очевидно неправда
Вы о чем?
Никто и не утверждал, что данные не хранят в union. Наоборот, основное назначение union - разделяемое использование памяти (т.к. экономия памяти) при хранении объемных данных. Именно для этого в подавляющем большинстве случаев и используется union. А хранение объемных данных обычно именно долгосрочно. Вы сами привели примеры такого использования.
Здесь же речь шла о совсем другом, побочном использовании union - использовании его для выполнения type-punning, то если для переинтерпретации памяти. (Переинтерпретация памяти, кстати, не является "кастом". Не ясно почему вы упомянули этот термин выше.)
Я вел речь именно об этом побочном использовании union. То есть никто не будет выбирать union для долгосрочного хранения именно из-за того, что где-то в какой-то момент кому-то может вдруг понадобится переинтерпретация.
А зачем std::launder в седьмом варианте? Можно же использовать указатель, возвращаемый new, не?
[[nodiscard]] float int_to_float7(int x) noexcept
{
return *new(&x) float;
}
Да, я вкурсе про mempy пешение. Но у меня на msvc 19 memcpy с /Ob2 не заменился. Добавление /Oi сделало лучше, memcpy превратилось в несколько инструкций, но не zero-cost.
Для простых случаев memcpy. Для сложных: а зачем?
Embedded и системное программирование, однако. Есть у вас порт, в котором много-много битов с разными значениями. Или бинарные протоколы хранения/обмена. Копирование может скорость в десятки раз понизить. Через макросы работать? Так и работают, но это чревато ошибками и не контролируется компилятором/статическими анализаторами.
Копирование может скорость в десятки раз понизить.
Вы это из головы взяли или реально замеряли?
Работает. Пока не перестанет.
Когда то и знаковое переполнение "работало", и реинтерпретация через каст, и указатели на локальные переменные "возвращались" и т.п.
Дойдут руки у авторов компиляторов - перестанет "работать".
Вы правы.
Но непонятно, кому оно мешало то? Если кто то возжелал иметь механизмы защиты от стрельбы, в ногу, то зачем делать это на активно используемом языке? Можно было создать свой язык в котором все кошерно, а не тихой сапой делать вереницу клонов что бы породить срвершенно иной язык.
У меня последние годы впечатление, что авторы компиляторов не делом заняты, а изобретением способов как бы им сломать людям код в неочевидных местах сохряняя при этом совместимость со стандартом.
Это - следствие того, что в массовых аппаратных архитектурах начали активно появляться и/или выходить на передний план фичи, которые получают существенную пользу от эксплуатации оптимизационных возможностей, кроющихся в неопределенном поведении. Это и векторизация, и многопоточность, и многоядерность, и еще много чего.
Людям испокон веков говорили, что этот день настанет и ваше пренебрежительное отношение к UB вернется, чтобы укусить вас за задницу. Они игнорировали эти предупреждения. Теперь не надо жаловаться, что кому-то там "сломали код".
Напомню, что Линус тогда встал на их сторону.
Линус не считал их код верным, он просто предложил вставить костыль со стороны компилятора что-бы заставить неправильно написанную, но популярную программу работать верно.
Только не в компиляторе, а в glibc. И конкретно, там была история, что все сломалось из-за того, что бравый парень из intel решил в рамках оптимизации, для ускорения копировать области памяти задом наперед. В результате generic версия работало нормально, а оптимизированная крашилась в Adobe Flash.
Немного позанудствую. Но если использовать типичную реализацию memcpy для копирования пересекающихся областей, то он будет работать лишь в одном случаи. Либо когда адрес destination < source, либо наоборот. Что по определению является дурно пахнущим кодом.
Так насколько я понимаю, если не брать всякие причуды стандарта, то memmove можно было бы и оставить, так как memcpy является ее строгим подмножетством. Вопрос условно говоря, в десятке тактов на определение не пересечения двух диапазонов.
Вопрос условно говоря, в десятке тактов на определение не пересечения двух диапазонов.
Не совсем. Если область пересечения диапазонов сопоставима по размеру с самими диапазонами, то копировать придётся небольшими кусочками.
В языке C значения типа enum неявно преобразуемы к типу
int
и обратно
Вроде как в C++ тоже так происходит. Для обхода такого свойства используется:
enum class Enumeration {
A,
B
};
C++11 позволяет ещё делать перечисления с типом, что избавляет от постоянного использования конструкции приведения типов:
enum Enumeration : uint32_t {
A = 1,
B = 2
};
Нет, в С++ так не происходит. enum class
запрещает даже неявное преобразование в направлении enum -> int. А преобразования int -> enum в С++ не было никогда.
Как это не было и нет, если обычный enum аналогично Си работает изначально, а enum class появился лишь в C++11?
Обычный enum в С++ не работает "аналогично Си изначально". Я же ясно написал: неявного преобразования int -> enum в С++ нет и не было никогда. О том и речь.
Просто попробуйте.
enum Enumeration {
A,
B,
C
};
int main()
{
int intvar = B;
printf("%d", intvar);
return 0;
}
Этот пример выводит цифру 1.
Наоборот:
enum Enumeration {
A,
B,
C
};
int main()
{
Enumeration enmvar;
enmvar = 1;
printf("%d", enmvar);
return 0;
}
конечно не получается. Но вы то пишите о том, что это в обе стороны не работает. Я же добавил, что enum class нужен, чтобы этого достичь.
Вы пытаетесь меня запутать. Я пока что утверждал следующее:
В С неявное преобразование между
enum
иint
работает в обе стороны
В С++ неявное преобразование между
enum
иint
работает только в одну сторону:enum -> int
В С++ неявное преобразование между
enum class
иint
вообще не работает ни в какую сторону
Что здесь не верно? И где я утверждал что-то другое?
P.S. Наверное, так можно проинтерпретировать текст самой публикации, где я не уточнил, что преобразование в одну сторону в С++ есть... Поправил текст.
char test[s]; // ERROR: expression must have a constant value
Еще раз: тема коллекции - правильный С код, который неправилен в С++. Именно в этом направлении.
Ваш пример интересен, но он "наоборот".
Еще можно использовать enum { s = 10 };
.
Если это С++ то constexpr
А вы каким стандартном собираете? В C99 это добавляли как обязательную фичу и должно собираться вроде.
Это, простите, что за IAR такой? Все более-менее распространённые (ARM, RISC-V, Renesas, ... ) умеют и C++17, и С99.
Там, правда, есть всякие полузаброшенные ветки типа STM8 и 8051, я не помню, какой стандарт они умеют (но с высокой вероятностью, C99 умеют).
Использование VLA в таких случаях - спорный совет, но тем не менее: а что за компилятор скрывается за этим IAR?
Язык C разрешает делать объявления новых типов внутри оператора приведения типа, внутри оператора
sizeof
, в объявлениях функций (типы возвращаемого значения и типы параметров)
Ещё и внутри составного литерала.
Ещё не упомянули Variable Length Arrays из C, которых нет в C++.
Я отношу такие фундаментальные фичи к "явным и очевидным" отличиям. Хотя, конечно, внешне они могут и не бросаться в глаза.
Честно говоря, я бы не назвал эту вещь "очевидной". Был довольно сильно удивлён, когда узнал о существовании этой фичи и того, как для неё похачили язык, который всегда подавался как максимально простой в реализации.
А в чем собственно проблема это сделать? Сгенерировать код, который из stack pointer'а отнимает не константу, а вычисляемое значение?
Там ради этого ещё сделали оператор sizeof в рантайме. Не то чтобы фичи реально сложные, понятное дело, но уже больше шансов, что наколеночные компиляторы сделают в этом месте что-то по-своему. Мне казалось, язык как раз и существует для того, чтобы его на любой платформе могли быстро поддержать, сделав свой компилятор.
Это опциональная возможность языка C. Фактически в C++ тоже самое: поддержка зависит от компилятора.
Еще restrict в С++ забыли завезти, что позволяет некоторым троллить "C быстрее C++" :)
Список интересный, но с практической точки зрения большинство пунктов - из категории вредных советов. Не совместимо с C++ - и плевать, все равно за такой код руки отрывают по самые уши.
Очевидное исключение, которое напрочь ломает подход типа "C++ - это надмножество C, просто возьми C код и скомпилируй как C++" - это неявное преобразование из void*. В обычной программе на С бывают сотни строчек типа `Foo* bar = malloc(sizeof(*bar))`, и все они ломают компиляцию в режиме C++, и в каждую надо добавить явный каст...
То же самое: restrict
- очевидная и "бросающееся в глаза" возможность о С99, по каковой причине в мой список она не включена. Это именно то, о чем я говорю в начале: не составит труда построить примеры С99 кода на основе новых ключевых слов. Меня это не интересовало.
в С++ вместо restrict правила по которым типы могут асиасится, то есть фактически автоматический restrict где нужно
Не совсем ясно.
Правила алиасинга в С ничем принципиально не отличаются от правил алиасинга в С++. Это тем не менее не делает restrict
бесполезным в С.
Контрпример неочевидной пессимизации: https://vector-of-bool.github.io/2022/05/11/char8-memset.html
В этом примере добавление __restrict__
пессимизацию исправляет (отдельная проблема в том, чтобы еще найти такие места). Может, чуток надуманный пример, но char* - очень часто используемый для работы с "сырой" памятью тип, и он алиасится во все PODы, равно как и std::byte* (и, что интересно, и std::uint8_t, хоть вроде бы по стандарту и не должен - но по факту он определяется как unsigned char). Единственный известный мне стандартный "байтовый" тип, который точно не алиасится с POD-ами, это char8_t
, который семантически, в отличие от того же byte, не предназначен для работы с "просто" памятью.
для этого и ввели char8_t
char8_t семантически - символьный тип для UTF-8 строк, и введен был для них. Его можно, конечно, заиспользовать и для обработки видеоданных, и для строк EBCDIC, но это еще большее извращение, чем использование unsigned char. Для обработки произвольных сырых данных были введены uint8_t, как числовой тип байтового размера, и byte, как нечисловой и несимвольный тип байтового размера - но как раз для них и надо помнить, что они алиасят всё в округе, и быть с ними осторожными.
uint8_t это алис на unsigned char, то есть то же самое и правила алиасинга те же
byte это алиасящийся со всеми тип для работы с собственно байтами.
Никакой особой безопасности соблюдать тут не нужно, вся работа безопасна, но может быть меньше оптимизаций из-за алиасинга. Опасности это не представляет.
В С11 ещё добавили:
Гарантируете что массив будет минимум 100 элементов.
void someFunction(char someArray[static 100])
И константный массив.
void someFunction(char someArray[const]);
Это появилось еще в C99. Опять же - это примеры "бросающихся в глаза" свойств, специфичных именно для C99, поэтому в свой список я их не включал. Практически весь мой список (за редкими исключениями) построен на свойствах "классического" C89/90.
Ваш второй пример - это не "константный массив". Для "константного массива" не нужно никакого специального синтаксисаvoid someFunction(const char someArray[]);
Приведенный же вами пример декларирует константность самого параметра-указателя, то есть эквивалентенvoid someFunction(char *const someArray);
Заинтересовало. Как эта фича называется, чтобы я погуглить мог? Есть у меня сейчас код на Си, где подобный синтаксис повысил бы читаемость раза в 2
В С++20 добавили designated initializers, так что в копилку хоть и достаточно очевидных, но не самых приятных несовместимостей можно добавить, что следующий код абсолютно корректен в Си, но сломается в С++
struct S {
int a;
int b;
};
int main(void) {
struct S s = {
.b = 3,
.a = 4,
};
return 0;
}
Плюсом можно дополнить, что в Сях поддерживается следующий синтаксис для designated initializers
int array[10] = {
[5] = 0xDEAD,
[8] = 0xBEEF,
};
В плюсах оно не работает (а обидно). Но это уже в копилку очевидных синтаксических несостыковок пойдет.
Первое различие с которым я встретился было объявление функции main
int main(argc,argv,arge)
int argc;
char ** argv,arge
{
...
return 0;
}
Т.е. типы передаваемых переменных определялись перед телом функции но после закрывающей круглой скобочки.
Элементы языка С, которые являются неподдерживаемыми в языке С++