Нижеприведенный список является моей небольшой коллекцией примеров кода на языке С, которые не являются корректными с точки зрения языка С++ или имеют какое-то специфичное именно для языка С поведение. (Именно в эту сторону: С код, являющийся некорректным с точки зрения С++.)

Этот материал я уже публиковал на другом ресурсе в менее причесанном виде, Я бы, наверное, поддался прокрастинации и никогда не собрался опубликовать эту коллекцию здесь, но из-за горизонта уже доносится стук копыт неумолимо приближающегося С23, который безжалостно принесет некоторые жемчужины моей коллекции в жертву богам С-С++ совместимости. Поэтому мне и пришлось встать с печи, пока они еще актуальны...

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

(Пометка [C23] помечает те пункты, которые станут неактуальными с выходом C23.)

  1. В языке C разрешается "терять" замыкающий \0 при инициализации массива символов строковым литералом:

    char s[4] = "1234";

    В С++ такая инициализация является некорректной.

  2. C поддерживает предварительные определения. В одной единице трансляции можно сделать множественные внешние определения одного и того же объекта без инициализатора:

    int a;
    int a;
    int a, a, a;

    Подобные множественные определения не допускаются в С++.

  3. Язык С разрешает определять внешние объекты неполных типов при условии, что тип доопределяется и становится полным где-то дальше в этой же единице трансляции:

    struct S s; 
    struct S { int i; };

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

    Вышеприведенная последовательность объявлений некорректна с точки зрения С++: язык С++ сразу запрещает определять объекты неполных типов.

  4. В языке C вы можете сделать неопределяющее объявление сущности неполного типа void.

    extern void v;

    (Соответствующее ему определение, однако, в C сделать не получится, т.к. void - неполный тип.)

    В C++ же не получится сделать даже неопределяющее объявление.

  5. Язык С допускает определение переменных с квалификатором const без явной инициализации:

    void foo(void)
    {
      const int a;
    }

    В C++ такое определение является некорректным.

  6. Язык 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++.

  7. В языке С "незнакомое" имя 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++.

  8. Язык C разрешает передачу управления в область видимости автоматической переменной, которое "перепрыгивает" через ее объявление с инициализацией:

    switch (1)
    {
      int a = 42;
    case 1:;
    }

    Такая передача управления недопустима с точки зрения C++.

  9. Начиная с C99 в языке C появились неявные блоки: некоторые инструкции сами по себе являются блоками и в дополнение к этому индуцируют вложенные подблоки. Например, и сам цикл forявляется блоком, и тело цикла является отдельным блоком, вложенным в блок цикла for. По этой причине следующий код является корректным в языке С:

    for (int i = 0; i < 10; ++i)
    { 
      int i = 42; 
    }

    Переменная i, объявленная в теле цикла, не имеет никакого отношения к переменной i, объявленной в заголовке цикла.

    В языке C++ в такой ситуации и заголовок цикла, и тело цикла образуют единую область видимости, что исключает возможность "вложенного" объявления i.

  10. Язык C допускает использование бессмысленных спецификаторов класса хранения в объявлениях, которые не объявляют никаких объектов:

    static struct S { int i; };

    В языке C++ такого не допускается.

    Дополнительно можно заметить, что в языке C typedef формально тоже является лишь одним из спецификаторов класса хранения, что позволяет создавать бессмысленные typedef-объявления, которые не объявляют псевдонимов:

    typedef struct S { int i; };

    C++ не допускает таких typedef-объявлений.

  11. Язык С допускает явные повторения cv-квалификаторов в объявлениях:

    const const const int a = 42;

    Код некорректен с точки зрения C++. (С++ тоже закрывает глаза на аналогичную избыточную квалификацию, но только через посредство промежуточных имен типов: typedef-имен, типовых параметров шаблонов).

  12. В языке C прямое копирование volatile объектов - не проблема (по крайней мере с точки зрения формальной корректности кода):

    void foo(void)
    {
      struct S { int i; }; 
      volatile struct S v = { 0 }; 
      struct S s = v;
      s = v;
    }

    В С++ же неявно генерируемые конструкторы копирования и операторы присваивания не принимают volatile объекты в качестве аргументов.

  13. В языке C любое целочисленное константное выражение со значением 0 может использоваться в качестве null pointer constant:

    void *p = 2 - 2;
    void *q = -0;

    Так же обстояли дела и в языке C++ до принятия стандарта C++11. Однако в современном C++ из целочисленных значений только буквальное нулевое значение (целочисленный литерал с нулевым значением) может выступать в роли null pointer constant, а вот более сложные выражения более не являются допустимыми. Вышеприведенные инициализации некорректны с точки зрения C++.

  14. В языке С не поддерживается 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 * невозможно.

  15. [C23] Препроцессор языка C не знаком с такими литералами как true и false. В языке C true и false доступны лишь как макросы, определенные в стандартном заголовке <stdbool.h>. Если эти макросы не определены, то в соответствии с правилами работы препроцессора, как #if true так и #if false должно вести себя как #if 0.

    В то же время препроцессор языка C++ обязан натурально распознавать литералы true и false и его директива #if должна вести себя с этим литералами "ожидаемым" образом.

    Это может служить источником несовместимостей, когда в C-коде не произведено включение <stdbool.h>:

    #if true
    int a[-1];
    #endif

    Данный код является заведомо некорректным в C++, и в то же время может спокойно компилироваться в C.

  16. Начиная с C++11 препроцессор языка C++ больше не рассматривает последовательность <литерал><идентификатор> как независимые лексемы. С точки зрения языка C++ <идентификатор> в такой ситуации является суффиксом литерала. Чтобы избежать такой интерпретации, в языке C++ эти лексемы следует разделять пробелом:

    #define D "d"
    
    int a = 42;
    printf("%"D, a);

    Такой формат для printf корректен c точки зрения C, но некорректен с точки зрения C++.

  17. Рекурсивные вызовы функции main разрешены в C, но запрещены в C++. Программам на С++ вообще не дозволяется никак использовать основную функцию main.

  18. В языке C строковые литералы имеют тип char [N], а в языке C++ - const char [N]. Даже если считать, что "старый" C++ в виде исключения поддерживает преобразование строкового литерала к типу char *, это исключение работает только тогда, когда оно применяется непосредственно к строковому литералу

    char *p = &"abcd"[0];

    Такая инициализация некорректна с точки зрения C++.

  19. В языке С битовое поле, объявленное с типом int без явного указания signed или unsigned может быть как знаковым, там и беззнаковым (определяется реализацией). В языке С++ такое битовое поле всегда является знаковым.

  20. В языке С typedef-имена типов и тэги struct-типов располагаются в разных пространствах имен и не конфликтуют друг с другом. Например, такой набор объявлений корректен с точки зрения языка С:

    struct A { int a; };
    typedef struct B { int b; } A;
    typedef struct C { int c; } C;

    В языке С++ не существует отдельного понятия тэга для класс-типов: имена классов разделяют одно пространство имен с typedef-именами и могут конфликтовать с ними. Для частичной совместимости с кодом на С язык С++ разрешает объявлять typedef-псевдонимы, совпадающие с именами существующих класс-типов, но только при условии, что псевдоним ссылается на класс-тип с точно таким же именем. В вышеприведенном примере typedef-объявление в строке 2 некорректно с точки зрения C++, а объявление в строке 3 - корректно.

  21. В языке С неявный конфликт между внутренним и внешним связыванием при объявлении одной и той же переменной приводит к неопределенному поведению, а в языке С++ такой конфликт делает программу ошибочной. Чтобы устроить такой конфликт, надо выстроить довольно хитрую конфигурацию

    static int a; /* Внутреннее связывание */
    
    void foo(void) 
    { 
      int a; /* Скрывает внешнее `a`, не имеет связывания */
    
      {
        extern int a; 
        /* Из-за того, что внешнее `a` скрыто, объявляет `a` с внешним 
           связыванием. Теперь `a` объявлено и с внешним, и с внутренним 
           связыванием - конфликт */ 
      } 
    }

    В С++ такое extern-объявление является ошибочным, Несмотря на то, что этой необычной ситуации посвящен отдельный пример в стандарте языка С++, популярные компиляторы С++ как правило не диагностируют это нарушение.

    Далее следуют примеры отличий, которые по моему мнению тривиальны, общеизвестны и неинтересны.

    Я их привожу здесь для полноты и, опять же, потому, что они формально удовлетворяют вышеприведенному критерию: на первый взгляд код выглядит более-менее нормально и в глазах для С++-наблюдателя.

  22. Язык C допускает неявное преобразование указателей из типа void *:

    void *p = 0;
    int *pp = p;
  23. В языке C значения типа enum неявно преобразуемы к типу int и обратно:

    enum E { A, B, C } e = A;
    e = e + 1;

    В С++ неявное преобразование работает только в одну сторону.

  24. [C23] Язык C поддерживает объявления функций без прототипов:

    void foo(); /* Объявление без прототипа */
    
    void bar() 
    { 
      foo(1, 2, 3); 
    }

  25. В языке C вложенные объявления struct-типов помещают имя внутреннего типа во внешнюю (охватывающую) область видимости:

    struct A 
    { 
      struct B { int b; } a;
    };
    
    struct B b; /* Сслыается на тип `struct B`, объявленный в строке 3 */

Вот, собственно, и все, что накопилось на текущий момент.

Комментарии (78)


  1. F0iL
    02.08.2022 08:06
    +17

    А как же классика: использование union для битовых кастов и обратно абсолютно допустимо в C, но является undefined behaviour согласно стандарту C++ (хотя некоторые компиляторы допускают такое в виде нестандартного расширения языка, ибо слишком уж много программистов думает, что так можно делать):

    typedef union my_union_u
    {
        uint32_t u;
        float f;
    } my_union_t;
    // в C такое нормально, а в C++ писать f и потом читать v и наоборот = нарушение active member rule


    1. lieff
      02.08.2022 10:57
      +1

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


      1. TheCalligrapher Автор
        02.08.2022 11:06

        В С++ специально для реинтерпретации есть std::bit_cast.


        1. netricks
          02.08.2022 11:09

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


          1. TheCalligrapher Автор
            02.08.2022 11:28
            +6

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


            1. netricks
              02.08.2022 11:33

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


            1. Arenoros
              03.08.2022 21:52

              всмысле не хранит? hello from каждая первая реализация json, yml, msgpack, он там конечно не используется для кастов но говорить что данные в union долгосрочно не хранят очевидно неправда


              1. TheCalligrapher Автор
                03.08.2022 22:02
                +1

                Вы о чем?

                Никто и не утверждал, что данные не хранят в union. Наоборот, основное назначение union - разделяемое использование памяти (т.к. экономия памяти) при хранении объемных данных. Именно для этого в подавляющем большинстве случаев и используется union. А хранение объемных данных обычно именно долгосрочно. Вы сами привели примеры такого использования.

                Здесь же речь шла о совсем другом, побочном использовании union - использовании его для выполнения type-punning, то если для переинтерпретации памяти. (Переинтерпретация памяти, кстати, не является "кастом". Не ясно почему вы упомянули этот термин выше.)

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


                1. Arenoros
                  03.08.2022 22:04

                  а, ну я видимо не правильно понял первое предложения


          1. F0iL
            02.08.2022 11:45
            +2

            Там под капотом std::memcpy (что и является единственным правильным вариантом решения такой задачи до C++20), и его практически все современные компиляторы умеют оптимизировать избегая реального копирования: https://godbolt.org/z/aEYnWvrnq


            1. NotSure
              02.08.2022 14:47

              А зачем std::launder в седьмом варианте? Можно же использовать указатель, возвращаемый new, не?

              [[nodiscard]] float int_to_float7(int x) noexcept

              {

                  return *new(&x) float;

              }


            1. lieff
              02.08.2022 15:18
              +1

              Да, я вкурсе про mempy пешение. Но у меня на msvc 19 memcpy с /Ob2 не заменился. Добавление /Oi сделало лучше, memcpy превратилось в несколько инструкций, но не zero-cost.


              1. dmitrmax
                03.08.2022 02:08

                Следуя тексту вашего оппонента, у вас практически несовременный компилятор)


      1. Deosis
        02.08.2022 11:18

        Для простых случаев memcpy. Для сложных: а зачем?


        1. Sap_ru
          02.08.2022 18:21
          +4

          Embedded и системное программирование, однако. Есть у вас порт, в котором много-много битов с разными значениями. Или бинарные протоколы хранения/обмена. Копирование может скорость в десятки раз понизить. Через макросы работать? Так и работают, но это чревато ошибками и не контролируется компилятором/статическими анализаторами.


          1. AnthonyMikh
            03.08.2022 23:30

            Копирование может скорость в десятки раз понизить.

            Вы это из головы взяли или реально замеряли?


            1. Sap_ru
              04.08.2022 00:03

              Это из личного опыта. При работе с железом или разборе бинарных протоколов на каждый пакет бывает нужно проанализировать/изменить десятки битов на каждый пакет.
              При работе с железом совсем всё грустно - время обработки прерываний может и сто раз увеличиться.


      1. F0iL
        02.08.2022 11:45
        +3

        Вариант с std::memcpy (единственно правильный до C++20) вполне нормально оптимизируется компиляторами и получается вполне zero-cost: https://godbolt.org/z/aEYnWvrnq


    1. netricks
      02.08.2022 11:07
      +2

      Будем честны, в с++, это все равно работает и компилируется.


      1. TheCalligrapher Автор
        02.08.2022 11:24
        +15

        Работает. Пока не перестанет.

        Когда то и знаковое переполнение "работало", и реинтерпретация через каст, и указатели на локальные переменные "возвращались" и т.п.

        Дойдут руки у авторов компиляторов - перестанет "работать".


        1. netricks
          02.08.2022 11:29
          +2

          Сколько же кода поломается…


          1. artemisia_borealis
            02.08.2022 12:28
            +4

            Лучше, чтобы стрёмный код сразу ломался, вместо поистине интригующих последствий UB в рандомное время.


            1. netricks
              02.08.2022 12:53
              +4

              Плюсы стремительно превращаются в то, чем они раньше не являлись.


              1. TheCalligrapher Автор
                02.08.2022 17:20

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


        1. Sergey_zx
          02.08.2022 12:29
          +2

          Вы правы.

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


        1. kekekeks
          02.08.2022 21:12
          +4

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


          1. TheCalligrapher Автор
            02.08.2022 21:24
            +13

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

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


      1. staticmain
        02.08.2022 13:57
        +3

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


        1. tyomitch
          02.08.2022 14:55
          +1

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


          1. TheRikipm
            02.08.2022 16:13

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


            1. thevlad
              02.08.2022 17:15
              +2

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


              1. TheRikipm
                03.08.2022 00:15
                +1

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

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


        1. thevlad
          02.08.2022 18:37

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


          1. staticmain
            02.08.2022 19:11
            +1

            Даже так нет гарантии, что memcpy не написан так, чтобы копировать каким-то извращенным способом типа спирали. Всё зависит от *libc, стандарт никак не описывает способ копирования, потому и разделяет эти две функции (иначе можно было бы оставить только одну memmove)


            1. thevlad
              02.08.2022 21:11
              +1

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


              1. AnthonyMikh
                03.08.2022 23:33

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

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


                1. thevlad
                  04.08.2022 00:01

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


  1. 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
    };


    1. TheCalligrapher Автор
      02.08.2022 09:35

      Нет, в С++ так не происходит. enum class запрещает даже неявное преобразование в направлении enum -> int. А преобразования int -> enum в С++ не было никогда.


      1. n7nexus
        02.08.2022 17:07
        +1

        Как это не было и нет, если обычный enum аналогично Си работает изначально, а enum class появился лишь в C++11?


        1. TheCalligrapher Автор
          02.08.2022 17:09
          +1

          Обычный enum в С++ не работает "аналогично Си изначально". Я же ясно написал: неявного преобразования int -> enum в С++ нет и не было никогда. О том и речь.

          Просто попробуйте.


          1. Koyanisqatsi
            02.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 нужен, чтобы этого достичь.


            1. TheCalligrapher Автор
              02.08.2022 18:27
              +3

              Вы пытаетесь меня запутать. Я пока что утверждал следующее:

              • В С неявное преобразование междуenum и int работает в обе стороны

              • В С++ неявное преобразование междуenum и int работает только в одну сторону: enum -> int

              • В С++ неявное преобразование междуenum class и int вообще не работает ни в какую сторону

              Что здесь не верно? И где я утверждал что-то другое?

              P.S. Наверное, так можно проинтерпретировать текст самой публикации, где я не уточнил, что преобразование в одну сторону в С++ есть... Поправил текст.


  1. predator86
    02.08.2022 13:53

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


    1. TheCalligrapher Автор
      02.08.2022 17:11
      +1

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

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


      1. predator86
        02.08.2022 17:17

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


        1. TheCalligrapher Автор
          02.08.2022 17:34
          +6

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


        1. Kelbon
          03.08.2022 10:01

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


          1. TheCalligrapher Автор
            03.08.2022 10:05
            +1

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


            1. Kelbon
              03.08.2022 10:15

              .


        1. dmitrmax
          03.08.2022 10:24

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


          1. predator86
            03.08.2022 15:11

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


            1. esaulenka
              04.08.2022 15:30

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


            1. TheCalligrapher Автор
              04.08.2022 17:21

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


              1. staticmain
                04.08.2022 18:56

                https://www.iar.com/products/architectures/arm/iar-embedded-workbench-for-arm/

                Свой собственный ЕМНИП. Как у Keil, у которого поддержки C99 не было до середины 2010х.


  1. eptr
    02.08.2022 16:05

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

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


  1. Denchick777
    02.08.2022 17:12
    +1

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


    1. TheCalligrapher Автор
      02.08.2022 17:14
      +1

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


      1. Denchick777
        02.08.2022 18:37
        +3

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


        1. dmitrmax
          03.08.2022 10:26

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


          1. Denchick777
            03.08.2022 10:56
            +1

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


            1. dmitrmax
              03.08.2022 11:01

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


    1. BareDreamer
      02.08.2022 18:19

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


      1. TheCalligrapher Автор
        02.08.2022 18:22
        +3

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


  1. arteast
    02.08.2022 19:11
    +2

    Еще restrict в С++ забыли завезти, что позволяет некоторым троллить "C быстрее C++" :)

    Список интересный, но с практической точки зрения большинство пунктов - из категории вредных советов. Не совместимо с C++ - и плевать, все равно за такой код руки отрывают по самые уши.

    Очевидное исключение, которое напрочь ломает подход типа "C++ - это надмножество C, просто возьми C код и скомпилируй как C++" - это неявное преобразование из void*. В обычной программе на С бывают сотни строчек типа `Foo* bar = malloc(sizeof(*bar))`, и все они ломают компиляцию в режиме C++, и в каждую надо добавить явный каст...


    1. TheCalligrapher Автор
      02.08.2022 19:25

      То же самое: restrict - очевидная и  "бросающееся в глаза" возможность о С99, по каковой причине в мой список она не включена. Это именно то, о чем я говорю в начале: не составит труда построить примеры С99 кода на основе новых ключевых слов. Меня это не интересовало.


    1. Kelbon
      03.08.2022 10:02

      в С++ вместо restrict правила по которым типы могут асиасится, то есть фактически автоматический restrict где нужно


      1. TheCalligrapher Автор
        03.08.2022 10:45

        Не совсем ясно.

        Правила алиасинга в С ничем принципиально не отличаются от правил алиасинга в С++. Это тем не менее не делает restrict бесполезным в С.


      1. 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, не предназначен для работы с "просто" памятью.


        1. Kelbon
          03.08.2022 11:26

          для этого и ввели char8_t


          1. arteast
            03.08.2022 12:04

            char8_t семантически - символьный тип для UTF-8 строк, и введен был для них. Его можно, конечно, заиспользовать и для обработки видеоданных, и для строк EBCDIC, но это еще большее извращение, чем использование unsigned char. Для обработки произвольных сырых данных были введены uint8_t, как числовой тип байтового размера, и byte, как нечисловой и несимвольный тип байтового размера - но как раз для них и надо помнить, что они алиасят всё в округе, и быть с ними осторожными.


            1. Kelbon
              03.08.2022 12:07

              uint8_t это алис на unsigned char, то есть то же самое и правила алиасинга те же

              byte это алиасящийся со всеми тип для работы с собственно байтами.

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


  1. alarih2_0
    02.08.2022 19:17
    +2

    В С11 ещё добавили:

    Гарантируете что массив будет минимум 100 элементов.

    void someFunction(char someArray[static 100])

    И константный массив.

    void someFunction(char someArray[const]);


    1. TheCalligrapher Автор
      02.08.2022 19:20

      Это появилось еще в C99. Опять же - это примеры "бросающихся в глаза" свойств, специфичных именно для C99, поэтому в свой список я их не включал. Практически весь мой список (за редкими исключениями) построен на свойствах "классического" C89/90.

      Ваш второй пример - это не "константный массив". Для "константного массива" не нужно никакого специального синтаксиса

      void someFunction(const char someArray[]);

      Приведенный же вами пример декларирует константность самого параметра-указателя, то есть эквивалентен
      void someFunction(char *const someArray);


  1. 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,
    };

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


    1. TheCalligrapher Автор
      03.08.2022 03:10

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


  1. eugeneyp
    03.08.2022 09:23

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

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

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


    1. TheCalligrapher Автор
      03.08.2022 10:04

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