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

Элементы языка С, которые являются неподдерживаемыми в языке С++

Время на прочтение7 мин
Количество просмотров32K

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

Этот материал я уже публиковал на другом ресурсе в менее причесанном виде, Я бы, наверное, поддался прокрастинации и никогда не собрался опубликовать эту коллекцию здесь, но из-за горизонта уже доносится стук копыт неумолимо приближающегося С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-объявлений.

    (Справедливости ради стоит заметить, что такие объявления в языке С не являются полностью бессмысленными: они все таки объявляют тип struct S.)

  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. В языке С разрешается использовать имя поля, совпадающее с существующим именем типа

    typedef int I;
    
    struct S
    {
      I I;
    };

    В языке С++ такое "переопределение" идентификатора не допускается.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Теги:
Хабы:
Всего голосов 104: ↑103 и ↓1+138
Комментарии80

Публикации

Истории

Работа

Программист С
33 вакансии

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн