Нижеприведенный список является моей небольшой коллекцией примеров кода на языке С, которые не являются корректными с точки зрения языка С++ или имеют какое-то специфичное именно для языка С поведение. (Именно в эту сторону: С код, являющийся некорректным с точки зрения С++.)
Этот материал я уже публиковал на другом ресурсе в менее причесанном виде, Я бы, наверное, поддался прокрастинации и никогда не собрался опубликовать эту коллекцию здесь, но из-за горизонта уже доносится стук копыт неумолимо приближающегося С23, который безжалостно принесет некоторые жемчужины моей коллекции в жертву богам С-С++ совместимости. Поэтому мне и пришлось встать с печи, пока они еще актуальны...
Разумеется, язык С имеет много существенных отличий от языка С++, т.е. не составит никакого труда привести примеры несовместимостей, основанные, скажем, на ключевых словах или других очевидных эксклюзивных свойствах С99. Таких примеров вы не найдете в списке ниже. Мой основной критерий для включения примеров в этот список заключался именно в том, что пример кода должен выглядеть на первый взгляд достаточно "невинно" для С++-наблюдателя, т.е. не содержать бросающихся в глаза С-эксклюзивов, но тем не менее являться специфичным именно для языка С.
(Пометка [C23] помечает те пункты, которые станут неактуальными с выходом C23.)
-
В языке C разрешается "терять" замыкающий
\0
при инициализации массива символов строковым литералом:char s[4] = "1234";
В С++ такая инициализация является некорректной.
-
C поддерживает предварительные определения. В одной единице трансляции можно сделать множественные внешние определения одного и того же объекта без инициализатора:
int a; int a; int a, a, a;
Подобные множественные определения не допускаются в С++.
-
Язык С разрешает определять внешние объекты неполных типов при условии, что тип доопределяется и становится полным где-то дальше в этой же единице трансляции:
struct S s; struct S { int i; };
На уровне обоснования эта возможность, скорее всего, является лишь следствием предыдущего пункта, т.е. возможности выполнять предварительные определения.
Вышеприведенная последовательность объявлений некорректна с точки зрения С++: язык С++ сразу запрещает определять объекты неполных типов.
-
В языке C вы можете сделать неопределяющее объявление сущности неполного типа
void
.extern void v;
(Соответствующее ему определение, однако, в C сделать не получится, т.к.
void
- неполный тип.)В C++ же не получится сделать даже неопределяющее объявление.
-
Язык С допускает определение переменных с квалификатором
const
без явной инициализации:void foo(void) { const int a; }
В C++ такое определение является некорректным.
-
Язык C разрешает делать объявления новых типов внутри оператора приведения типа, внутри оператора
sizeof
, в объявлениях функций (типы возвращаемого значения и типы параметров):int a = sizeof(enum E { A, B, C }) + (enum X { D, E, F }) 0; /* Дальнейший код использует объявления, сделанные выше */ enum E e = B; int b = e + F;
Такие объявления не допускаются в C++.
-
В языке С "незнакомое" имя struct-типа, упомянутое в списке параметров функции, является объявлением нового типа, локального для этой функции. При этом в списке параметров функции этот тип может быть объявлен как неполный, а "дообъявлен" до полного типа уже в теле функции:
/* Пусть тип `struct S` в этой точке еще не объявлен */ void foo(struct S *p) /* Первое упоминание `struct S` */ { struct S { int a; } s; /* Это все тот же `struct S` */ p = &s; p->a = 5; }
В этом коде все корректно с точки зрения языка С:
p
имеет тот же тип, что и&s
и содержит полеa
.С точки зрения языка C++ упоминание "незнакомого" имени класс-типа в списке параметров функции тоже является объявлением нового типа. Однако этот новый тип не является локальным: он считается принадлежащим охватывающему пространству имен. Поэтому с точки зрения языка C++ локальное определение типа
S
в теле функции не имеет никакого отношения к типуS
, упомянутому в списке параметров. Присваиваниеp = &s
невозможно из-за несоответствия типов. Вышеприведенный код некорректен с точки зрения C++. -
Язык C разрешает передачу управления в область видимости автоматической переменной, которое "перепрыгивает" через ее объявление с инициализацией:
switch (1) { int a = 42; case 1:; }
Такая передача управления недопустима с точки зрения C++.
-
Начиная с C99 в языке C появились неявные блоки: некоторые инструкции сами по себе являются блоками и в дополнение к этому индуцируют вложенные подблоки. Например, и сам цикл
for
является блоком, и тело цикла является отдельным блоком, вложенным в блок циклаfor
. По этой причине следующий код является корректным в языке С:for (int i = 0; i < 10; ++i) { int i = 42; }
Переменная
i
, объявленная в теле цикла, не имеет никакого отношения к переменнойi
, объявленной в заголовке цикла.В языке C++ в такой ситуации и заголовок цикла, и тело цикла образуют единую область видимости, что исключает возможность "вложенного" объявления
i
. -
Язык C допускает использование бессмысленных спецификаторов класса хранения в объявлениях, которые не объявляют никаких объектов:
static struct S { int i; };
В языке C++ такого не допускается.
Дополнительно можно заметить, что в языке Ctypedef
формально тоже является лишь одним из спецификаторов класса хранения, что позволяет создавать бессмысленные typedef-объявления, которые не объявляют псевдонимов:typedef struct S { int i; };
C++ не допускает таких typedef-объявлений. -
Язык С допускает явные повторения cv-квалификаторов в объявлениях:
const const const int a = 42;
Код некорректен с точки зрения C++. (С++ тоже закрывает глаза на аналогичную избыточную квалификацию, но только через посредство промежуточных имен типов: typedef-имен, типовых параметров шаблонов).
-
В языке C прямое копирование volatile объектов - не проблема (по крайней мере с точки зрения формальной корректности кода):
void foo(void) { struct S { int i; }; volatile struct S v = { 0 }; struct S s = v; s = v; }
В С++ же неявно генерируемые конструкторы копирования и операторы присваивания не принимают volatile объекты в качестве аргументов.
-
В языке C любое целочисленное константное выражение со значением
0
может использоваться в качестве null pointer constant:void *p = 2 - 2; void *q = -0;
Так же обстояли дела и в языке C++ до принятия стандарта C++11. Однако в современном C++ из целочисленных значений только буквальное нулевое значение (целочисленный литерал с нулевым значением) может выступать в роли null pointer constant, а вот более сложные выражения более не являются допустимыми. Вышеприведенные инициализации некорректны с точки зрения C++.
-
В языке С не поддерживается cv-квалификация для rvalues. В частности, cv-квалификация возвращаемого значения функции сразу же игнорируется языком. Вкупе с автоматическим преобразованием массивов к указателям, это позволяет обходить некоторые правила константной корректности:
struct S { int a[10]; }; const struct S foo() { struct S s; return s; } int main() { int *p = foo().a; }
Стоит заметить, однако, что попытка модификации rvalue в языке С приводит к неопределенному поведению.
С точки зрения языка C++ же возвращаемое значение
foo()
и, следовательно, массивfoo().a
, сохрaняют const-квалификацию, и неявное преобразованиеfoo().a
к типуint *
невозможно. -
[C23] Препроцессор языка C не знаком с такими литералами как
true
иfalse
. В языке Ctrue
иfalse
доступны лишь как макросы, определенные в стандартном заголовке<stdbool.h>
. Если эти макросы не определены, то в соответствии с правилами работы препроцессора, как#if true
так и#if false
должно вести себя как#if 0
.В то же время препроцессор языка C++ обязан натурально распознавать литералы
true
иfalse
и его директива#if
должна вести себя с этим литералами "ожидаемым" образом.Это может служить источником несовместимостей, когда в C-коде не произведено включение
<stdbool.h>
:#if true int a[-1]; #endif
Данный код является заведомо некорректным в C++, и в то же время может спокойно компилироваться в C.
-
Начиная с C++11 препроцессор языка C++ больше не рассматривает последовательность
<литерал><идентификатор>
как независимые лексемы. С точки зрения языка C++<идентификатор>
в такой ситуации является суффиксом литерала. Чтобы избежать такой интерпретации, в языке C++ эти лексемы следует разделять пробелом:#define D "d" int a = 42; printf("%"D, a);
Такой формат для
printf
корректен c точки зрения C, но некорректен с точки зрения C++. Рекурсивные вызовы функции
main
разрешены в C, но запрещены в C++. Программам на С++ вообще не дозволяется никак использовать основную функциюmain
.-
В языке C строковые литералы имеют тип
char [N]
, а в языке C++ -const char [N]
. Даже если считать, что "старый" C++ в виде исключения поддерживает преобразование строкового литерала к типуchar *
, это исключение работает только тогда, когда оно применяется непосредственно к строковому литералуchar *p = &"abcd"[0];
Такая инициализация некорректна с точки зрения C++. В языке С битовое поле, объявленное с типом
int
без явного указанияsigned
илиunsigned
может быть как знаковым, там и беззнаковым (определяется реализацией). В языке С++ такое битовое поле всегда является знаковым.-
В языке С typedef-имена типов и тэги struct-типов располагаются в разных пространствах имен и не конфликтуют друг с другом. Например, такой набор объявлений корректен с точки зрения языка С:
struct A { int a; }; typedef struct B { int b; } A; typedef struct C { int c; } C;
В языке С++ не существует отдельного понятия тэга для класс-типов: имена классов разделяют одно пространство имен с typedef-именами и могут конфликтовать с ними. Для частичной совместимости с кодом на С язык С++ разрешает объявлять typedef-псевдонимы, совпадающие с именами существующих класс-типов, но только при условии, что псевдоним ссылается на класс-тип с точно таким же именем. В вышеприведенном примере typedef-объявление в строке 2 некорректно с точки зрения C++, а объявление в строке 3 - корректно.
-
В языке С неявный конфликт между внутренним и внешним связыванием при объявлении одной и той же переменной приводит к неопределенному поведению, а в языке С++ такой конфликт делает программу ошибочной. Чтобы устроить такой конфликт, надо выстроить довольно хитрую конфигурацию
static int a; /* Внутреннее связывание */ void foo(void) { int a; /* Скрывает внешнее `a`, не имеет связывания */ { extern int a; /* Из-за того, что внешнее `a` скрыто, объявляет `a` с внешним связыванием. Теперь `a` объявлено и с внешним, и с внутренним связыванием - конфликт */ } }
В С++ такое extern-объявление является ошибочным, Несмотря на то, что этой необычной ситуации посвящен отдельный пример в стандарте языка С++, популярные компиляторы С++ как правило не диагностируют это нарушение.
Далее следуют примеры отличий, которые по моему мнению тривиальны, общеизвестны и неинтересны.
Я их привожу здесь для полноты и, опять же, потому, что они формально удовлетворяют вышеприведенному критерию: на первый взгляд код выглядит более-менее нормально и в глазах для С++-наблюдателя. -
Язык C допускает неявное преобразование указателей из типа
void *
:void *p = 0; int *pp = p;
-
В языке C значения типа enum неявно преобразуемы к типу
int
и обратно:enum E { A, B, C } e = A; e = e + 1;
В С++ неявное преобразование работает только в одну сторону.
-
[C23] Язык C поддерживает объявления функций без прототипов:
void foo(); /* Объявление без прототипа */ void bar() { foo(1, 2, 3); }
-
В языке C вложенные объявления struct-типов помещают имя внутреннего типа во внешнюю (охватывающую) область видимости:
struct A { struct B { int b; } a; }; struct B b; /* Сслыается на тип `struct B`, объявленный в строке 3 */
Вот, собственно, и все, что накопилось на текущий момент.
Комментарии (78)
Koyanisqatsi
02.08.2022 09:25В языке C значения типа enum неявно преобразуемы к типу
int
и обратноВроде как в C++ тоже так происходит. Для обхода такого свойства используется:
enum class Enumeration { A, B };
C++11 позволяет ещё делать перечисления с типом, что избавляет от постоянного использования конструкции приведения типов:
enum Enumeration : uint32_t { A = 1, B = 2 };
TheCalligrapher Автор
02.08.2022 09:35Нет, в С++ так не происходит.
enum class
запрещает даже неявное преобразование в направлении enum -> int. А преобразования int -> enum в С++ не было никогда.n7nexus
02.08.2022 17:07+1Как это не было и нет, если обычный enum аналогично Си работает изначально, а enum class появился лишь в C++11?
TheCalligrapher Автор
02.08.2022 17:09+1Обычный enum в С++ не работает "аналогично Си изначально". Я же ясно написал: неявного преобразования int -> enum в С++ нет и не было никогда. О том и речь.
Просто попробуйте.
Koyanisqatsi
02.08.2022 17:54-2enum 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 нужен, чтобы этого достичь.
TheCalligrapher Автор
02.08.2022 18:27+3Вы пытаетесь меня запутать. Я пока что утверждал следующее:
В С неявное преобразование между
enum
иint
работает в обе стороны
В С++ неявное преобразование между
enum
иint
работает только в одну сторону:enum -> int
В С++ неявное преобразование между
enum class
иint
вообще не работает ни в какую сторону
Что здесь не верно? И где я утверждал что-то другое?
P.S. Наверное, так можно проинтерпретировать текст самой публикации, где я не уточнил, что преобразование в одну сторону в С++ есть... Поправил текст.
predator86
02.08.2022 13:53const int s = 10;
char test[s]; // ERROR: expression must have a constant valueTheCalligrapher Автор
02.08.2022 17:11+1Еще раз: тема коллекции - правильный С код, который неправилен в С++. Именно в этом направлении.
Ваш пример интересен, но он "наоборот".
predator86
02.08.2022 17:17Может подскажите, что можно использовать кроме #define s 10?
Kelbon
03.08.2022 10:01Если это С++ то constexpr
TheCalligrapher Автор
03.08.2022 10:05+1В C++ и вопроса бы такого не возникло, ибо там исходный вариант - корректен.
dmitrmax
03.08.2022 10:24А вы каким стандартном собираете? В C99 это добавляли как обязательную фичу и должно собираться вроде.
predator86
03.08.2022 15:11На IAR. Там только «С89», или «Standard C». Поэтому без #define никак.
esaulenka
04.08.2022 15:30Это, простите, что за IAR такой? Все более-менее распространённые (ARM, RISC-V, Renesas, ... ) умеют и C++17, и С99.
Там, правда, есть всякие полузаброшенные ветки типа STM8 и 8051, я не помню, какой стандарт они умеют (но с высокой вероятностью, C99 умеют).
TheCalligrapher Автор
04.08.2022 17:21Использование VLA в таких случаях - спорный совет, но тем не менее: а что за компилятор скрывается за этим IAR?
staticmain
04.08.2022 18:56https://www.iar.com/products/architectures/arm/iar-embedded-workbench-for-arm/
Свой собственный ЕМНИП. Как у Keil, у которого поддержки C99 не было до середины 2010х.
eptr
02.08.2022 16:05Язык C разрешает делать объявления новых типов внутри оператора приведения типа, внутри оператора
sizeof
, в объявлениях функций (типы возвращаемого значения и типы параметров)Ещё и внутри составного литерала.
Denchick777
02.08.2022 17:12+1Ещё не упомянули Variable Length Arrays из C, которых нет в C++.
TheCalligrapher Автор
02.08.2022 17:14+1Я отношу такие фундаментальные фичи к "явным и очевидным" отличиям. Хотя, конечно, внешне они могут и не бросаться в глаза.
Denchick777
02.08.2022 18:37+3Честно говоря, я бы не назвал эту вещь "очевидной". Был довольно сильно удивлён, когда узнал о существовании этой фичи и того, как для неё похачили язык, который всегда подавался как максимально простой в реализации.
dmitrmax
03.08.2022 10:26А в чем собственно проблема это сделать? Сгенерировать код, который из stack pointer'а отнимает не константу, а вычисляемое значение?
Denchick777
03.08.2022 10:56+1Там ради этого ещё сделали оператор sizeof в рантайме. Не то чтобы фичи реально сложные, понятное дело, но уже больше шансов, что наколеночные компиляторы сделают в этом месте что-то по-своему. Мне казалось, язык как раз и существует для того, чтобы его на любой платформе могли быстро поддержать, сделав свой компилятор.
dmitrmax
03.08.2022 11:01Если вы обладаете квалификацией для написания собственного компилятора, то почему это для вас представляет сложность, не понимаю? Ну хорошо, оператор sizeof в runtime берет результат из переменной, тоже хранящейся на стэке.
BareDreamer
02.08.2022 18:19Это опциональная возможность языка C. Фактически в C++ тоже самое: поддержка зависит от компилятора.
TheCalligrapher Автор
02.08.2022 18:22+3Начиная с C23 поддержка variably modified types становится обязательной. Опциональной останется лишь возможность создавать автоматические объекты такого типа, то есть создавать VLA в стеке.
arteast
02.08.2022 19:11+2Еще restrict в С++ забыли завезти, что позволяет некоторым троллить "C быстрее C++" :)
Список интересный, но с практической точки зрения большинство пунктов - из категории вредных советов. Не совместимо с C++ - и плевать, все равно за такой код руки отрывают по самые уши.
Очевидное исключение, которое напрочь ломает подход типа "C++ - это надмножество C, просто возьми C код и скомпилируй как C++" - это неявное преобразование из void*. В обычной программе на С бывают сотни строчек типа `Foo* bar = malloc(sizeof(*bar))`, и все они ломают компиляцию в режиме C++, и в каждую надо добавить явный каст...
TheCalligrapher Автор
02.08.2022 19:25То же самое:
restrict
- очевидная и "бросающееся в глаза" возможность о С99, по каковой причине в мой список она не включена. Это именно то, о чем я говорю в начале: не составит труда построить примеры С99 кода на основе новых ключевых слов. Меня это не интересовало.
Kelbon
03.08.2022 10:02в С++ вместо restrict правила по которым типы могут асиасится, то есть фактически автоматический restrict где нужно
TheCalligrapher Автор
03.08.2022 10:45Не совсем ясно.
Правила алиасинга в С ничем принципиально не отличаются от правил алиасинга в С++. Это тем не менее не делаетrestrict
бесполезным в С.
arteast
03.08.2022 11:20Контрпример неочевидной пессимизации: 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, не предназначен для работы с "просто" памятью.Kelbon
03.08.2022 11:26для этого и ввели char8_t
arteast
03.08.2022 12:04char8_t семантически - символьный тип для UTF-8 строк, и введен был для них. Его можно, конечно, заиспользовать и для обработки видеоданных, и для строк EBCDIC, но это еще большее извращение, чем использование unsigned char. Для обработки произвольных сырых данных были введены uint8_t, как числовой тип байтового размера, и byte, как нечисловой и несимвольный тип байтового размера - но как раз для них и надо помнить, что они алиасят всё в округе, и быть с ними осторожными.
Kelbon
03.08.2022 12:07uint8_t это алис на unsigned char, то есть то же самое и правила алиасинга те же
byte это алиасящийся со всеми тип для работы с собственно байтами.
Никакой особой безопасности соблюдать тут не нужно, вся работа безопасна, но может быть меньше оптимизаций из-за алиасинга. Опасности это не представляет.
alarih2_0
02.08.2022 19:17+2В С11 ещё добавили:
Гарантируете что массив будет минимум 100 элементов.
void someFunction(char someArray[static 100])
И константный массив.
void someFunction(char someArray[const]);
TheCalligrapher Автор
02.08.2022 19:20Это появилось еще в C99. Опять же - это примеры "бросающихся в глаза" свойств, специфичных именно для C99, поэтому в свой список я их не включал. Практически весь мой список (за редкими исключениями) построен на свойствах "классического" C89/90.
Ваш второй пример - это не "константный массив". Для "константного массива" не нужно никакого специального синтаксисаvoid someFunction(const char someArray[]);
Приведенный же вами пример декларирует константность самого параметра-указателя, то есть эквивалентен
void someFunction(char *const someArray);
ReadOnlySadUser
03.08.2022 02:12+1В С++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, };
В плюсах оно не работает (а обидно). Но это уже в копилку очевидных синтаксических несостыковок пойдет.
TheCalligrapher Автор
03.08.2022 03:10А, понял... Вы имеете в виду, что, несмотря на выраженную внешнюю похожесть designated initializers в С и С++, их спецификации все таки существенно отличаются. Да, верно. Это формально соответствует моим критериям. Просто фича эта в С++ все еще производит впечатление "слишком новой"...
eugeneyp
03.08.2022 09:23Первое различие с которым я встретился было объявление функции main
int main(argc,argv,arge) int argc; char ** argv,arge { ... return 0; }
Т.е. типы передаваемых переменных определялись перед телом функции но после закрывающей круглой скобочки.
TheCalligrapher Автор
03.08.2022 10:04Определения функция в стиле K&R - хрестоматийная фича "классического" C. Это все является derecated с C89/90 и покидает язык окончательно в C23.
F0iL
А как же классика: использование union для битовых кастов и обратно абсолютно допустимо в C, но является undefined behaviour согласно стандарту C++ (хотя некоторые компиляторы допускают такое в виде нестандартного расширения языка, ибо слишком уж много программистов думает, что так можно делать):
lieff
А у этой проблемы с union на С++ есть стандартное zero-cost решение?
TheCalligrapher Автор
В С++ специально для реинтерпретации есть
std::bit_cast
.netricks
Оно не zero cost. Там копирование под капотом.
TheCalligrapher Автор
Так и в С никто не хранит данные в юнионах долгосрочно именно потому, что может вдруг понадобиться реинтерпретация. Такой юнион возникает на сцене кратковременно, именно для этой цели. И исходные данные в него приходится загружать, а переинтерпретированный результат - выгружать.
netricks
Пожалуй, что так.
Arenoros
всмысле не хранит? hello from каждая первая реализация json, yml, msgpack, он там конечно не используется для кастов но говорить что данные в union долгосрочно не хранят очевидно неправда
TheCalligrapher Автор
Вы о чем?
Никто и не утверждал, что данные не хранят в union. Наоборот, основное назначение union - разделяемое использование памяти (т.к. экономия памяти) при хранении объемных данных. Именно для этого в подавляющем большинстве случаев и используется union. А хранение объемных данных обычно именно долгосрочно. Вы сами привели примеры такого использования.
Здесь же речь шла о совсем другом, побочном использовании union - использовании его для выполнения type-punning, то если для переинтерпретации памяти. (Переинтерпретация памяти, кстати, не является "кастом". Не ясно почему вы упомянули этот термин выше.)
Я вел речь именно об этом побочном использовании union. То есть никто не будет выбирать union для долгосрочного хранения именно из-за того, что где-то в какой-то момент кому-то может вдруг понадобится переинтерпретация.
Arenoros
а, ну я видимо не правильно понял первое предложения
F0iL
Там под капотом std::memcpy (что и является единственным правильным вариантом решения такой задачи до C++20), и его практически все современные компиляторы умеют оптимизировать избегая реального копирования: https://godbolt.org/z/aEYnWvrnq
NotSure
А зачем std::launder в седьмом варианте? Можно же использовать указатель, возвращаемый new, не?
[[nodiscard]] float int_to_float7(int x) noexcept
{
return *new(&x) float;
}
lieff
Да, я вкурсе про mempy пешение. Но у меня на msvc 19 memcpy с /Ob2 не заменился. Добавление /Oi сделало лучше, memcpy превратилось в несколько инструкций, но не zero-cost.
dmitrmax
Следуя тексту вашего оппонента, у вас практически несовременный компилятор)
Deosis
Для простых случаев memcpy. Для сложных: а зачем?
Sap_ru
Embedded и системное программирование, однако. Есть у вас порт, в котором много-много битов с разными значениями. Или бинарные протоколы хранения/обмена. Копирование может скорость в десятки раз понизить. Через макросы работать? Так и работают, но это чревато ошибками и не контролируется компилятором/статическими анализаторами.
AnthonyMikh
Вы это из головы взяли или реально замеряли?
Sap_ru
Это из личного опыта. При работе с железом или разборе бинарных протоколов на каждый пакет бывает нужно проанализировать/изменить десятки битов на каждый пакет.
При работе с железом совсем всё грустно - время обработки прерываний может и сто раз увеличиться.
F0iL
Вариант с std::memcpy (единственно правильный до C++20) вполне нормально оптимизируется компиляторами и получается вполне zero-cost: https://godbolt.org/z/aEYnWvrnq
netricks
Будем честны, в с++, это все равно работает и компилируется.
TheCalligrapher Автор
Работает. Пока не перестанет.
Когда то и знаковое переполнение "работало", и реинтерпретация через каст, и указатели на локальные переменные "возвращались" и т.п.
Дойдут руки у авторов компиляторов - перестанет "работать".
netricks
Сколько же кода поломается…
artemisia_borealis
Лучше, чтобы стрёмный код сразу ломался, вместо поистине интригующих последствий UB в рандомное время.
netricks
Плюсы стремительно превращаются в то, чем они раньше не являлись.
TheCalligrapher Автор
Это давно началось. Джавизация С++. Еще со Страуструпа с его давнишним "везде используйте
std::vector
вместо массива". Слава Богу, именно это безобразие не прижилось - появилисьstd::array
и прочие способы "спасти" zero-cost подход. Но тенденция такая сохраняется.Sergey_zx
Вы правы.
Но непонятно, кому оно мешало то? Если кто то возжелал иметь механизмы защиты от стрельбы, в ногу, то зачем делать это на активно используемом языке? Можно было создать свой язык в котором все кошерно, а не тихой сапой делать вереницу клонов что бы породить срвершенно иной язык.
kekekeks
У меня последние годы впечатление, что авторы компиляторов не делом заняты, а изобретением способов как бы им сломать людям код в неочевидных местах сохряняя при этом совместимость со стандартом.
TheCalligrapher Автор
Это - следствие того, что в массовых аппаратных архитектурах начали активно появляться и/или выходить на передний план фичи, которые получают существенную пользу от эксплуатации оптимизационных возможностей, кроющихся в неопределенном поведении. Это и векторизация, и многопоточность, и многоядерность, и еще много чего.
Людям испокон веков говорили, что этот день настанет и ваше пренебрежительное отношение к UB вернется, чтобы укусить вас за задницу. Они игнорировали эти предупреждения. Теперь не надо жаловаться, что кому-то там "сломали код".
staticmain
Точно такие же аргументы были у программистов Adobe когда они использовали memcpy для пересекающихся областей в Flash.
tyomitch
Напомню, что Линус тогда встал на их сторону.
TheRikipm
Линус не считал их код верным, он просто предложил вставить костыль со стороны компилятора что-бы заставить неправильно написанную, но популярную программу работать верно.
thevlad
Только не в компиляторе, а в glibc. И конкретно, там была история, что все сломалось из-за того, что бравый парень из intel решил в рамках оптимизации, для ускорения копировать области памяти задом наперед. В результате generic версия работало нормально, а оптимизированная крашилась в Adobe Flash.
TheRikipm
Что было разрешено спецификацией, прошу заметить.
thevlad
Немного позанудствую. Но если использовать типичную реализацию memcpy для копирования пересекающихся областей, то он будет работать лишь в одном случаи. Либо когда адрес destination < source, либо наоборот. Что по определению является дурно пахнущим кодом.
staticmain
Даже так нет гарантии, что memcpy не написан так, чтобы копировать каким-то извращенным способом типа спирали. Всё зависит от *libc, стандарт никак не описывает способ копирования, потому и разделяет эти две функции (иначе можно было бы оставить только одну memmove)
thevlad
Так насколько я понимаю, если не брать всякие причуды стандарта, то memmove можно было бы и оставить, так как memcpy является ее строгим подмножетством. Вопрос условно говоря, в десятке тактов на определение не пересечения двух диапазонов.
AnthonyMikh
Не совсем. Если область пересечения диапазонов сопоставима по размеру с самими диапазонами, то копировать придётся небольшими кусочками.
thevlad
Так вопрос был в том можно ли заменить все на memmove. 10 тактов это как раз примерно столько, сколько надо для того чтобы посчитать, что диапазоны не пересекаются и использовать максимально оптимизированную реализацию memcpy, иначе стандартную memmove.