Какие элементы С являются неподдерживаемыми в С++?

1,00
р.
Какие элементы языка С являются неподдерживаемыми в С++? Какой код на С не будет принят компилятором С++? Особенно интересует поведение g++.

Ответ
Язык C существенно отличается от языка C++ с самого начала его существования. (Понятно, что новые свойства языка C99 позволяют нам запросто писать примеры C кода, который не будет компилироваться в C++, но на самом деле для этого совсем не обязательно обращаться к C99. Даже "классический" стандартный C - C89/90 - заметно отличается от C++.)
Серьезных отличий много, но согласно постановке вопроса нас интересуют только отличия, которые делают корректный C код некорректным в C++. Не претендуя на полноту, попробую перечислить эти отличия и привести примеры кода, опирающиеся на эти отличия. Ключевым моментом здесь является именно то, что использованные синтаксические конструкции присутствуют в обоих языках, т.е. с первого взгляда код выглядит достаточно невинно и с точки зрения языка C++.
В языке C разрешается "терять" замыкающий '\0' при инициализации массива символов строковым литералом char s[4] = "1234"
Код некорректен с точки зрения C++.
C поддерживает "предварительные" (tentative) определения. В одной единице трансляции можно сделать множественные внешние определения одного и того же объекта /* На уровне файла */
int a int a int a, a, a
Код некорректен с точки зрения C++.
В языке С typedef-имена типов и тэги struct-типов располагаются в разных пространствах имен и не конфликтуют друг с другом. Например, такой набор объявлений допустим в С struct A { int i } typedef struct B { int i } A typedef struct C { int i } C
В языке С++ не существует отдельного понятия "тэга" для класс-типов: имена классов разделяют одно пространство имен с typedef-именами и могут конфликтовать с ними. Для частичной обратной совместимости с кросс-компилируемым идиоматическим С кодом язык С++ разрешает объявлять typedef-псевдонимы, совпадающие с именами существующих класс-типов, но только при условии, что псевдоним ссылается именно на класс-тип с таким же именем. В вышеприведенном примере первое typedef-объявление некорректно с точки зрения C++.
В языке С "незнакомое" имя struct типа, упомянутое в списке параметров функции, является объявлением нового типа, локального для этой функции. При этом в списке параметров функции этот тип может быть объявлен как неполный, а "дообъявлен" до полного типа уже в теле функции /* Тип `struct S` в этой точке не известен */
void foo(struct S *p) { struct S { int a } s
p = &s p->a = 5 }
В этом коде все корректно с точки зрения языка С: p имеет тот же тип, что и &s и т.д. С точки зрения языка C++ упоминание "незнакомого" имени struct типа в списке параметров функции тоже является объявлением нового типа. Однако этот новый тип не является локальным: он считается принадлежащим охватывающему пространству имен. Поэтому с точки зрения языка C++ локальное определение типа S в вышеприведенном примере не имеет никакого отношения к типу S, упомянутому в списке параметров. Присваивание p = &s невозможно из-за несоответствия типов. Код некорректен с точки зрения 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 S s struct S { int i }
Вышеприведенный набор объявлений корректен с точки зрения С, но некорректен с точки зрения С++. Язык С++ безусловно запрещает определять объекты неполных типов.
В языке C многие statements создают свою неявную охватывающую область видимости в дополнение к уже существующей области видимости в "теле" этого statement, в то время как в C++ создается единая область видимости. Например for (int i = 0 i < 10 ++i) { int i = 42 }
В языке C тело цикла является вложенной областью видимости по отношению к заголовку цикла, вследствие чего данный код является корректным. В C++ область видимости только одна, что исключает возможность "вложенного" объявления i.
Язык C разрешает прыжки через объявления с инициализацией switch (1) { int a = 42 case 1: }
Код некорректен с точки зрения C++.
В C вложенные объявления struct типов помещают имя внутреннего типа во внешнюю (охватывающую) область видимости struct A { struct B { int b } a }
struct B b
Код некорректен с точки зрения C++.
C допускает неявное преобразование указателей из типа void * void *p = 0 int *pp = p
Код некорректен с точки зрения C++.
C поддерживает объявления функций без прототипов /* На уровне файла */
void foo()
void bar() { foo(1, 2, 3) }
Код некорректен с точки зрения C++.
В C значения типа enum свободно неявно преобразуемы к типу int и обратно enum E { A, B, C } e = A e = e + 1
Код некорректен с точки зрения C++.
Неявно генерируемые компилятором C++ конструкторы копирования и операторы присваивания не умеют копировать volatile объекты. В C же копирование volatile объектов - не проблема struct S { int i } volatile struct S v = { 0 } struct S s = v
Код некорректен с точки зрения C++.
В C строковые литералы имеют тип char [N], а в C++ - const char [N]. Даже если считать, что "классический" C++ в виде исключения поддерживает преобразование строкового литерала к типу char *, это исключение работает только тогда, когда оно применяется непосредственно к строковому литералу char *p = &"abcd"[0]
Код некорректен с точки зрения C++.
C допускает использование "бессмысленных" спецификаторов класса хранения в объявлениях, которые не объявляют объектов static struct S { int i }
Код некорректен с точки зрения C++. Дополнительно можно заметить, что в языке C typedef формально тоже является лишь одним из спецификаторов класса хранения, что позволяет создавать typedef-объявления, которые не объявляют псевдонимов typedef struct S { int i }
C++ не допускает таких typedef-объявлений.
С допускает явные повторения cv-квалификаторов в объявлениях const const int a = 42
Код некорректен с точки зрения C++. (С++ допускает аналогичную "избыточную" квалификацию, но только через посредство промежуточных имен типов: typedef-имена, параметры шаблонов).
В языке C любое целочисленное константное выражение со значением 0 может использоваться в качестве null pointer constant void *p = 2 - 2 void *q = -0
Так же обстояли дела и в C++ до принятия стандарта C++11. Однако в современном C++ из целочисленных значений только буквальное значение 0 (целочисленный литерал) может выступать в роли null pointer constant, а вот более сложные выражения более не являются допустимыми. Вышеприведенные инициализации некорректны с точки зрения C++.
В языке C вы можете сделать неопределяющее объявление объекта типа void extern void v
(Определение такого объекта сделать не получится, т.к. void - неполный тип). В C++ запрещается делать даже неопределяющее объявление.
В языке С битовое поле, объявленное с типом int без явного указания signed или unsigned может быть как знаковым, там и беззнаковым (определяется реализацией). В языке С++ такое битовое поле всегда является знаковым.
Препроцессор языка C не знаком с такими литералами как true и false. В языке C true и false доступны лишь как макросы, определенные в заголовочном файле . Если эти макросы не определены, то в соответствии с правилами работы препроцессора, как #if true так и #if false должно вести себя как #if 0. В то же время препроцессор языка C++ обязан нативно распознавать литералы true и false и его директива #if должна вести себя с этим литералами "ожидаемым" образом. Это может служить источником несовместимостей, когда в C-коде не произведено включение #if true int a[-1] #endif
Данный код является заведомо некорректным в C++, и в то же время может спокойно компилироваться в C.
В языке С не поддерживается cv-квалификация для rvalues. В частности, cv-квалификация возвращаемого значения функции игнорируется языком. Вкупе с автоматическим преобразованием массивов к указателям, это позволяет обходить некоторые правила константной корректности struct S { int a[10] }
const struct S foo() { struct S s return s }
int main() { int *p = foo().a }
С точки зрения языка C++ возвращаемое значение foo() и, следовательно, массив foo().a являются константными, и неявное преобразование foo().a к типу int * невозможно.
В языке С неявный конфликт между внутренним и внешним связыванием при объявлении одной и той же сущности приводит к неопределенному поведению, а в С++ такой конфликт делает программу ill-formed (ошибочной). Чтобы устроить такой неявный конфликт, надо выстроить довольно хитрую конфигурацию static int a /* Внутреннее связывание */
void foo(void) { int a /* Скрывает внешнее static `a`, не имеет связывания */
{ extern int a /* Из-за того, что внешнее static `a` скрыто, объявляет `a` с внешним связыванием. Теперь `a` объявлено и с внешним, и с внутренним связыванием - конфликт */ } }
В С++ такое extern-объявление является ошибочным.
Рекурсивные вызовы функции main разрешены в C, но запрещены в C++.
Препроцессор языка C++ больше не рассматривает (C++11) последовательность <строковый или символьный литерал><идентификатор> как независимые токены. С точки зрения языка C++ <идентификатор> в такой ситуации является суффиксом литерала. Чтобы избежать такой интерпретации, в языке C++ эти токены следует разделять пробелом uint32_t a = 42 printf("%"PRIu32, a)
Этот код корректен c точки зрения C, но некорректен с точки зрения C++.
Язык С допускает определение константных объектов без инициализации void foo() { const int a }
В C++ такое объявление является некорректным.
В языке С разрешается использовать имя поля, совпадающее с существующим именем типа typedef int I
struct S { I I }
В языке С++ такое "переопределение" идентификатора не допускается.