Нижеприведенный список является моей небольшой коллекцией примеров кода на языке С, которые не являются корректными с точки зрения языка С++ или имеют какое-то специфичное именно для языка С поведение. (Именно в эту сторону: С код, являющийся некорректным с точки зрения С++.)
Этот материал я уже публиковал на другом ресурсе в менее причесанном виде, Я бы, наверное, поддался прокрастинации и никогда не собрался опубликовать эту коллекцию здесь, но из-за горизонта уже доносится стук копыт неумолимо приближающегося С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)
 - Koyanisqatsi02.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 в С++ не было никогда. - n7nexus02.08.2022 17:07+1- Как это не было и нет, если обычный enum аналогично Си работает изначально, а enum class появился лишь в C++11?  - TheCalligrapher Автор02.08.2022 17:09+1- Обычный enum в С++ не работает "аналогично Си изначально". Я же ясно написал: неявного преобразования int -> enum в С++ нет и не было никогда. О том и речь. - Просто попробуйте.  - Koyanisqatsi02.08.2022 17:54-2- 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 нужен, чтобы этого достичь.  - TheCalligrapher Автор02.08.2022 18:27+3- Вы пытаетесь меня запутать. Я пока что утверждал следующее: - В С неявное преобразование между - enumи- intработает в обе стороны
 - В С++ неявное преобразование между - enumи- intработает только в одну сторону:- enum -> int
- В С++ неявное преобразование между - enum classи- intвообще не работает ни в какую сторону
 - Что здесь не верно? И где я утверждал что-то другое? - P.S. Наверное, так можно проинтерпретировать текст самой публикации, где я не уточнил, что преобразование в одну сторону в С++ есть... Поправил текст. 
 
 
 
 
 
 - predator8602.08.2022 13:53- const int s = 10; 
 char test[s]; // ERROR: expression must have a constant value - TheCalligrapher Автор02.08.2022 17:11+1- Еще раз: тема коллекции - правильный С код, который неправилен в С++. Именно в этом направлении. - Ваш пример интересен, но он "наоборот".  - predator8602.08.2022 17:17- Может подскажите, что можно использовать кроме #define s 10?  - Kelbon03.08.2022 10:01- Если это С++ то constexpr  - TheCalligrapher Автор03.08.2022 10:05+1- В C++ и вопроса бы такого не возникло, ибо там исходный вариант - корректен. 
 
  - dmitrmax03.08.2022 10:24- А вы каким стандартном собираете? В C99 это добавляли как обязательную фичу и должно собираться вроде.  - predator8603.08.2022 15:11- На IAR. Там только «С89», или «Standard C». Поэтому без #define никак.  - esaulenka04.08.2022 15:30- Это, простите, что за IAR такой? Все более-менее распространённые (ARM, RISC-V, Renesas, ... ) умеют и C++17, и С99. 
 Там, правда, есть всякие полузаброшенные ветки типа STM8 и 8051, я не помню, какой стандарт они умеют (но с высокой вероятностью, C99 умеют).
  - TheCalligrapher Автор04.08.2022 17:21- Использование VLA в таких случаях - спорный совет, но тем не менее: а что за компилятор скрывается за этим IAR?  - staticmain04.08.2022 18:56- https://www.iar.com/products/architectures/arm/iar-embedded-workbench-for-arm/ - Свой собственный ЕМНИП. Как у Keil, у которого поддержки C99 не было до середины 2010х. 
 
 
 
 
 
 
 - eptr02.08.2022 16:05- Язык C разрешает делать объявления новых типов внутри оператора приведения типа, внутри оператора - sizeof, в объявлениях функций (типы возвращаемого значения и типы параметров)- Ещё и внутри составного литерала. 
 - Denchick77702.08.2022 17:12+1- Ещё не упомянули Variable Length Arrays из C, которых нет в C++.  - TheCalligrapher Автор02.08.2022 17:14+1- Я отношу такие фундаментальные фичи к "явным и очевидным" отличиям. Хотя, конечно, внешне они могут и не бросаться в глаза.  - Denchick77702.08.2022 18:37+3- Честно говоря, я бы не назвал эту вещь "очевидной". Был довольно сильно удивлён, когда узнал о существовании этой фичи и того, как для неё похачили язык, который всегда подавался как максимально простой в реализации.  - dmitrmax03.08.2022 10:26- А в чем собственно проблема это сделать? Сгенерировать код, который из stack pointer'а отнимает не константу, а вычисляемое значение?  - Denchick77703.08.2022 10:56+1- Там ради этого ещё сделали оператор sizeof в рантайме. Не то чтобы фичи реально сложные, понятное дело, но уже больше шансов, что наколеночные компиляторы сделают в этом месте что-то по-своему. Мне казалось, язык как раз и существует для того, чтобы его на любой платформе могли быстро поддержать, сделав свой компилятор.  - dmitrmax03.08.2022 11:01- Если вы обладаете квалификацией для написания собственного компилятора, то почему это для вас представляет сложность, не понимаю? Ну хорошо, оператор sizeof в runtime берет результат из переменной, тоже хранящейся на стэке. 
 
 
 
 
  - BareDreamer02.08.2022 18:19- Это опциональная возможность языка C. Фактически в C++ тоже самое: поддержка зависит от компилятора.  - TheCalligrapher Автор02.08.2022 18:22+3- Начиная с C23 поддержка variably modified types становится обязательной. Опциональной останется лишь возможность создавать автоматические объекты такого типа, то есть создавать VLA в стеке. 
 
 
 - arteast02.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 кода на основе новых ключевых слов. Меня это не интересовало.
  - Kelbon03.08.2022 10:02- в С++ вместо restrict правила по которым типы могут асиасится, то есть фактически автоматический restrict где нужно  - TheCalligrapher Автор03.08.2022 10:45- Не совсем ясно. 
 Правила алиасинга в С ничем принципиально не отличаются от правил алиасинга в С++. Это тем не менее не делает- restrictбесполезным в С.
  - arteast03.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, не предназначен для работы с "просто" памятью. - Kelbon03.08.2022 11:26- для этого и ввели char8_t  - arteast03.08.2022 12:04- char8_t семантически - символьный тип для UTF-8 строк, и введен был для них. Его можно, конечно, заиспользовать и для обработки видеоданных, и для строк EBCDIC, но это еще большее извращение, чем использование unsigned char. Для обработки произвольных сырых данных были введены uint8_t, как числовой тип байтового размера, и byte, как нечисловой и несимвольный тип байтового размера - но как раз для них и надо помнить, что они алиасят всё в округе, и быть с ними осторожными.  - Kelbon03.08.2022 12:07- uint8_t это алис на unsigned char, то есть то же самое и правила алиасинга те же - byte это алиасящийся со всеми тип для работы с собственно байтами. - Никакой особой безопасности соблюдать тут не нужно, вся работа безопасна, но может быть меньше оптимизаций из-за алиасинга. Опасности это не представляет. 
 
 
 
 
 
 - alarih2_002.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);
 
 - ReadOnlySadUser03.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 в С и С++, их спецификации все таки существенно отличаются. Да, верно. Это формально соответствует моим критериям. Просто фича эта в С++ все еще производит впечатление "слишком новой"... 
 
 - eugeneyp03.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.