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

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

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

А у этой проблемы с union на С++ есть стандартное zero-cost решение?

Оно не zero cost. Там копирование под капотом.

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

Пожалуй, что так.

всмысле не хранит? 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 в рандомное время.
Плюсы стремительно превращаются в то, чем они раньше не являлись.

Это давно началось. Джавизация С++. Еще со Страуструпа с его давнишним "везде используйте std::vector вместо массива". Слава Богу, именно это безобразие не прижилось - появились std::array и прочие способы "спасти" zero-cost подход. Но тенденция такая сохраняется.

Вы правы.

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

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

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

Людям испокон веков говорили, что этот день настанет и ваше пренебрежительное отношение к UB вернется, чтобы укусить вас за задницу. Они игнорировали эти предупреждения. Теперь не надо жаловаться, что кому-то там "сломали код".

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

Напомню, что Линус тогда встал на их сторону.

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

Только не в компиляторе, а в glibc. И конкретно, там была история, что все сломалось из-за того, что бравый парень из intel решил в рамках оптимизации, для ускорения копировать области памяти задом наперед. В результате generic версия работало нормально, а оптимизированная крашилась в Adobe Flash.

 бравый парень из intel решил в рамках оптимизации, для ускорения копировать области памяти задом наперед

Что было разрешено спецификацией, прошу заметить.

Немного позанудствую. Но если использовать типичную реализацию memcpy для копирования пересекающихся областей, то он будет работать лишь в одном случаи. Либо когда адрес destination < source, либо наоборот. Что по определению является дурно пахнущим кодом.

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

Так насколько я понимаю, если не брать всякие причуды стандарта, то memmove можно было бы и оставить, так как memcpy является ее строгим подмножетством. Вопрос условно говоря, в десятке тактов на определение не пересечения двух диапазонов.

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

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

Так вопрос был в том можно ли заменить все на memmove. 10 тактов это как раз примерно столько, сколько надо для того чтобы посчитать, что диапазоны не пересекаются и использовать максимально оптимизированную реализацию memcpy, иначе стандартную memmove.

В языке 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. Наверное, так можно проинтерпретировать текст самой публикации, где я не уточнил, что преобразование в одну сторону в С++ есть... Поправил текст.

const int s = 10;
char test[s]; // ERROR: expression must have a constant value

Еще раз: тема коллекции - правильный С код, который неправилен в С++. Именно в этом направлении.

Ваш пример интересен, но он "наоборот".

Может подскажите, что можно использовать кроме #define s 10?

Еще можно использовать enum { s = 10 };.

Если это С++ то constexpr

В C++ и вопроса бы такого не возникло, ибо там исходный вариант - корректен.

А вы каким стандартном собираете? В C99 это добавляли как обязательную фичу и должно собираться вроде.

На IAR. Там только «С89», или «Standard C». Поэтому без #define никак.

Это, простите, что за IAR такой? Все более-менее распространённые (ARM, RISC-V, Renesas, ... ) умеют и C++17, и С99.
Там, правда, есть всякие полузаброшенные ветки типа STM8 и 8051, я не помню, какой стандарт они умеют (но с высокой вероятностью, C99 умеют).

Использование VLA в таких случаях - спорный совет, но тем не менее: а что за компилятор скрывается за этим IAR?

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

Язык C разрешает делать объявления новых типов внутри оператора приведения типа, внутри оператора sizeof, в объявлениях функций (типы возвращаемого значения и типы параметров)

Ещё и внутри составного литерала.

Ещё не упомянули Variable Length Arrays из C, которых нет в C++.

Я отношу такие фундаментальные фичи к "явным и очевидным" отличиям. Хотя, конечно, внешне они могут и не бросаться в глаза.

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

А в чем собственно проблема это сделать? Сгенерировать код, который из stack pointer'а отнимает не константу, а вычисляемое значение?

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

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

Это опциональная возможность языка C. Фактически в C++ тоже самое: поддержка зависит от компилятора.

Начиная с C23 поддержка variably modified types становится обязательной. Опциональной останется лишь возможность создавать автоматические объекты такого типа, то есть создавать VLA в стеке.

Еще 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,
};

В плюсах оно не работает (а обидно). Но это уже в копилку очевидных синтаксических несостыковок пойдет.

А, понял... Вы имеете в виду, что, несмотря на выраженную внешнюю похожесть designated initializers в С и С++, их спецификации все таки существенно отличаются. Да, верно. Это формально соответствует моим критериям. Просто фича эта в С++ все еще производит впечатление "слишком новой"...

Первое различие с которым я встретился было объявление функции main

int main(argc,argv,arge)
int argc;
char ** argv,arge
{
 ...
 return 0;
}

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

Определения функция в стиле K&R - хрестоматийная фича "классического" C. Это все является derecated с C89/90 и покидает язык окончательно в C23.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории