Как известно, C и C++ — это родственные языки между которыми есть много общего. Но их пути, с годами, расходятся всё сильнее. В общих чертах дело обстоит так: код, написанный на одном из этих языков, не может быть скомпилирован под видом кода, написанного на другом. Этому мешает множество больших и маленьких различий между языками. Причём, речь идёт не только о синтаксических различиях. Некоторые общие синтаксические конструкции могут иметь разную семантику. Поэтому обычно нет никакого смысла в том, чтобы компилировать код, написанный на C, с помощью C++-компилятора. Не стоит доверять тем, кто утверждает обратное.
Правда, есть одна сфера, где обычно наблюдается согласие между C и C++. Это — ABI (Application Binary Interface, двоичный интерфейс приложений). Структуры данных и функции одного языка могут быть, в той или иной мере, использованы в другом языке. C и C++, кроме того, достаточно сильно пересекаются в области спецификаций интерфейсов, вследствие чего один и тот же заголовочный файл можно использовать из кода, написанного на обоих языках.
В этом материале я постараюсь собрать то общее, что есть у C и C++ и предложу некоторые рекомендации, которые позволят достаточно хорошо сочетать эти языки. Это, правда, будет всего лишь рассказ, иллюстрирующий точку зрения C-программиста, который хочет создавать интерфейсы для C++.
Типы данных
В C и C++ используется один и тот же набор базовых типов данных и конструкций, которые можно применять и там и там. А вот при работе с некоторыми типами нужно учитывать то, что в них присутствуют небольшие различия.
Целочисленные типы — такие, как char
, short
, int
, long
в обоих языках имеют схожие представление и семантику. Но нужно знать о том, что у типа bool
(или _Bool
) и у перечислений есть некоторые особенности, о которых мы поговорим ниже.
Все типы для чисел с плавающей запятой имеют одно и то же представление. При работе с ними используется одинаковый синтаксис. Это — float
, double
, и, возможно, long double
. Но синтаксис комплексных типов в C и C++ различается. В C имеется нечто вроде спецификатора, указывающего на реальный базовый тип, а в C++ для описания таких типов используются шаблоны.
Массивы имеют одинаковый синтаксис и представление. Но C позволяет менять размер массива, давая возможность работать с так называемыми массивами переменной длины (Variable Length Arrays, VLA).
Типы struct
и union
, если в них не объявляются члены-функции, имеют одно и то же представление. В C++ их называют простыми структурами данных (Plain Old Data structures, POD).
Атомарные типы имеют одно и то же представление и семантику, но разный синтаксис. В C++ используются шаблоны. В C применяются квалификаторы и спецификаторы типов.
▍Логический тип данных
Логический тип данных в C «официально» называется _Bool
, но существует удобный макрос, в котором объявлено ключевое слово bool
, указывающее на этот тип. На самом деле, эта конструкция была создана только для обеспечения обратной совместимости с кодом, который был написан до введения логического типа. Её вполне могут убрать из будущих версий стандарта C. В C++ в использовании _Bool
особого смысла нет, выглядит эта конструкция нехорошо и представляет собой введение в C++-код возможности C, которая является временной (хотя в таком статусе она уже пребывает очень и очень долго).
Поэтому легче всего для организации работы с логическими значениями просто пользоваться bool
и при этом обращать внимание на то, чтобы C видел бы соответствующий заголовочный файл. Сделать это несложно:
#ifndef __cplusplus
# include
#endif
extern bool weAreHappy;
Как уже было сказано, подключать этот заголовочный файл в C++ не имеет никакого смысла.
▍Перечисления
Простые перечисления должны обрабатываться в C и C++ одинаково. Константы перечисления имеют одинаковые значения, но разные типы. В C это — тип int
, в C++ — это тип самого перечисления.
До тех пор, пока константы используются ради их значений, всё должно работать хорошо. Но если попытаться воспользоваться переменными или параметрами функций, имеющих перечисляемый тип, начнутся сложности. В C и C++ используются разные правила для неявного преобразования значений к таким типам и для преобразования значений таких типов к другим типам. Поэтому лучше их в таком качестве не использовать.
▍Атомарные типы
В C++ атомарный вариант некоего базового типа описывают в виде шаблона:
extern std::atomic flags;
В C имеется два варианта объявления подобных типов:
extern unsigned _Atomic flags; // квалификатор _Atomic
extern _Atomic(unsigned) flags; // спецификатор _Atomic
В коде, который подойдёт и для C и для C++, можно пользоваться вторым из предыдущих двух вариантов:
#ifdef __cplusplus
# include
# define _Atomic(T) std::atomic
#else
# include
#endif
extern _Atomic(unsigned) flags;
▍Комплексные типы
В C++ комплексные типы, опять же, объявляют в виде шаблонных типов:
extern std::complex angle;
В C эквивалент этой конструкции выглядит так:
extern complex double angle;
Оба языка гарантируют то, что эти типы представлены двумя последовательно расположенными объектами базового типа. Первый — для действительной части комплексного числа, второй — для мнимой.
К сожалению, в данном случае не существует синтаксической конструкции, подобной спецификатору _Atomic
, о котором мы говорили выше. Это позволило бы пользоваться простым макросом. С другой стороны, существует не так много комплексных типов. Поэтому спасти положение может простое объявление таких типов прямо в коде программы:
#ifdef __cplusplus
# include
typedef std::complex cfloat;
typedef std::complex cdouble;
typedef std::complex cldouble;
# define I (cfloat({ 0, 1 }))
#else
# include
typedef complex float cfloat;
typedef complex double cdouble;
typedef complex long double cldouble;
#endif
extern cdouble angle;
...
cdouble angle = 4.0 + 3.0*I;
Кроме того, нужно учитывать то, что код, в котором применяются комплексные типы C и C++, должен использовать идентификатор I
только для указания комплексного корня из -1.
Объекты
Совместимость C и C++ на уровне ABI означает, что объекты с полями любых типов, общих для этих языков, имеют одно и то же представление. То есть — одинаковое размещение в памяти данных этих объектов и их одинаковую интерпретацию. С синтаксической точки зрения именованные объекты — то есть — переменные и параметры функций, объявляются одинаково. В противном случае сама идея спецификации общего интерфейса была бы безнадёжной.
▍Временные объекты
Надо отметить, что C и C++ очень сильно различаются в используемом в них понятии неименованных, временных объектов. В C существует два вида временных объектов:
- Возвращаемые значения функций, которые содержат массивы, представляют собой временные объекты, что позволяет обращаться к элементам массива с использованием механизмов адресной арифметики. Подобные временные объекты неизменяемы и прекращают существовать после того, как вычисление выражения, содержащего вызов функции, завершается.
- Составные литералы — временные объекты, которые создают явным образом. Они работают как переменные, объявленные в текущей области видимости, к ним обычно применимы все правила, применимые к подобным переменным.
В C++ нет эквивалента второго из вышеописанных видов временных объектов. Даже временные объекты, которые создают явным образом, вызывая конструктор, обычно существуют лишь в процессе вычисления выражения, в котором они появляются. Правда, если сохранить ссылку на такой объект — это может увеличить срок его жизни, но тут применяются очень сложные правила, и работа со ссылками, в любом случае, выходит за пределы темы общего интерфейса между C и C++.
Поэтому временные объекты в интерфейсах, как правило, лучше не использовать в тех случаях, когда они могли бы применяться в C для передачи адреса объекта функции, которая вернёт этот адрес для дальнейшего использования в текущей области видимости.
Мы ещё вернёмся к этой теме в разделах о списках аргументов переменной длины и о макросах.
▍Объекты с квалификатором const
В C++ объекты с квалификатором const
можно помещать в заголовочные файлы, что приводит к тому, что они ведут себя как константы базового типа.
constexpr unsigned const fortytwo = 42u;
Эта конструкция (даже без constexpr
) не разрешена в C, так как её использование приведёт к объявлению объекта fortytwo
во всех .o-файлах, а это приведёт к нарушению правила одного определения. Если, так сказать, сэмулировать эту возможность, объявив объект с использованием ключевого слова static
, это приведёт к следующим последствиям:
- Будет создано несколько копий объекта, размещённых в разных местах памяти. Программы, которые сравнивают указатели на подобные объекты, могут работать неправильно.
- В C объекты, объявленные с ключевым словом
static
, нельзя использовать из функций, объявленных с ключевым словомinline
.
При создании межъязыковых интерфейсов мы пользуемся макросами. Для типов, представленных литералами, всё выглядит довольно просто:
#define fortytwo 42u
Для структурных типов нет единого решения этой проблемы. В C используются составные литералы в макросе, в C++ применяется глобальный объект с квалификатором const
.
Функции
Если речь идёт о функциях, относящихся к тем общим для C и C++ типам, о которых мы говорили выше, то на некоей платформе ABI вызова таких функций должен быть одинаковым. Это не зависит от языка, через который мы обращаемся к интерфейсу. Тут применяются одинаковые правила представления параметров функции и возвращаемых значений в аппаратных регистрах или в стеке. Первое важное различие между C и C++ заключается в том, что в C++ есть механизм перегрузки функций. Поэтому должно выполняться преобразование типов аргументов во внешней функции — если только не будет указано, что делать этого не нужно. Вот распространённая конструкция для реализации такого поведения:
#ifdef __cplusplus
extern "C" {
#endif
int toto(void);
double hui(char*);
#ifdef __cplusplus
}
#endif
Здесь имеются две C++-зоны, окружающие спецификацию общего интерфейса, объявляющего функцию с использованием ключевого слова extern
с языковым интерфейсом C. Макрос __cplusplus
при использовании любого C-компилятора гарантированно окажется необъявленным, а при использовании любого C++-компилятора — объявленным.
▍Функции без параметров
В C интерфейс функции, который объявляется с пустым списком параметров, на самом деле, не имеет прототипа. Такая функция может принимать любое количество параметров (почти) любых типов. Существуют особые правила о том, как аргументы подобных функций преобразуются на вызывающей стороне.
Функция, которая действительно не принимает никаких аргументов, должна быть определена, в результате, особым образом. В частности, с использованием void
в списке параметров. Например — это функция toto
из вышеприведённого примера.
▍Функции со списком параметров переменной длины
Вот распространённая конструкция, используемая в C для описания функций, принимающих 2-мерные массивы:
void initialize(size_t n, size_t m, double A[n][m]);
В C++ нет синтаксических конструкций, поддерживающих эту возможность. Поэтому мы не можем пользоваться подобными функциями в описаниях интерфейсов между C и C++.
В теории, правда, C++ может использовать и подобные функции, так как ABI этой функции представляет собой два значения size_t
и указатель на значение double
. Информация о типе матрицы в C собирается в начале выполнения функции, у вызывающей стороны нет ничего, что она могла бы предоставить функции в дополнение к аргументам.
У нас может возникнуть желание предоставить «фиктивный» интерфейс для C++, которому нужно лишь значение типа double
, но подобное, при неправильном использовании функции, может легко привести к негативным последствиям. Лучше всего создать небольшую обёртку, которая принимает подходящий шаблон типа vector
.
▍Параметры, представленные многомерными массивами с квалификатором const
В C и C++ используются разные правила совместимости типов с различными квалификаторами. В C функцию с параметром, представленным 2-мерным массивом, объявленным с квалификатором const
, нелегко вызвать с аргументом, объявленным без const
. В межъязыковых интерфейсах этим лучше не пользоваться. Правда, можно пользоваться указателями на сущности, объявленные с использованием const
.
▍Квалификатор restrict и псевдонимы
В C и C++ используются разные правила работы с псевдонимами. Так и должно быть из-за имеющейся в C++ концепции ссылки. Это значит, что следует проявлять большую осторожность в принятии решения о том, какими свойствами должен обладать указатель. В C есть возможность объявлять указатели с квалификатором restrict
. Это приводит к тому, что к объекту, адрес которого хранит указатель, можно обращаться только посредством этого указателя. Это — мощная возможность, которая налагает важные ограничения на сторону, вызывающую функцию.
В C++ нет механизма, аналогичного этому. Часто по этому поводу выдвигают рекомендацию, в соответствии с которой при использовании C++ нужно просто объявить restrict
в виде макроса, ничем не заменяемого. Но из-за этого теряется смысл информирования того, что вызывает функцию, о необходимости внимательно следить за тем, что передаётся функции, передавая разные аргументы в разные параметры.
Постарайтесь, если возможно, не пользоваться этим в межъязыковых интерфейсах. Если же без этого не обойтись — тщательно документируйте тот факт, что аргументы функции должны быть различными.
▍Функции, объявленные с ключевым словом inline
Семантика ключевого слова inline
немного отличается в C и C++, но обычно, соблюдая осторожность, им можно пользоваться. Разница заключается в создании экземпляра функции в том случае, когда компоновщику нужна её копия. C++ занимается этим сам, а в C задача по предоставлению системе в точности одного экземпляра соответствующего кода ложится на программиста. Такой код должен располагаться в одном из связанных друг с другом объектных файлов. Поэтому, пользуясь этой возможностью, нужно проконтролировать, чтобы библиотека, реализующая интерфейс, предоставляла бы подобный код.
Но функции, объявленные с ключевым словом inline
, это, в первую очередь, функции. Поэтому к ним применимо всё вышесказанное о различиях между C и C++. Если вы пользуетесь этой возможностью — постарайтесь сделать функции как можно меньше и переложите большую часть реализации их внутренних механизмов на возможности одного из языков.
▍Вариадические функции
Вариадические функции — это сложная тема. При работе с ними применяются непростые правила по преобразованию (продвижению) типов аргументов, у них нет внутреннего механизма, позволяющего узнать о количестве полученных аргументов. Не стоит создавать новые интерфейсы, использующие эту возможность. В результате использование этой возможности стоит ограничить несколькими стандартными функциями библиотеки C — вроде printf
или scanf
.
Оба языка имеют возможности, способные, в большинстве случаев, заменить вариадические функции. Но, конечно, использование вариадических шаблонов C++ выходит за рамки создания межъязыковых интерфейсов с C. Обычно в случаях, когда речь идёт о списке аргументов одного и того же типа, при объявлении которых используется квалификатор const
, в промежуточных вызовах макроса может использоваться параметр, представленный временным массивом.
▍Универсальные интерфейсы функций
В C и C++ используются диаметрально противоположные стратегии при реализации универсальных интерфейсов функций. В C++ имеется механизм перегрузки функций и аргументы, применяемые по умолчанию. В C есть первичные выражения _Generic
, для реализации которых используются макросы. Механизмы, используемые в C, нелегко расширять, то есть — можно объединить лишь функции для известного списка типов. Если нужно поддерживать новый тип — нужно изменить выражение _Generic
или макрос.
Тут у C и C++ не особенно много общего. Поэтому, если нужно реализовать подобную возможность, рассчитанную на оба языка, нужно реализовывать её по-разному для каждого из них.
В качестве достаточно простого решения этой задачи можно воспользоваться созданием разных функций для списка типов в C с использованием подходящего соглашения об именовании сущностей. При таком подходе имена, например, могут выглядеть как hu_flt
и hu_dbl
. В C будет использоваться макрос:
#define hu(X)
_Generic((X),
float: hu_flt,
double: hu_dbl)
(X)
В C++ будет просто использоваться интерфейс с несколькими конкретизациями шаблона:
template inline auto hu(T x);
template inline auto hu(float x) { return hu_flt(x); }
template inline auto hu(double x) { return hu_dbl(x); }
И тот и другой подход приведут к появлению одинаково эффективного исполняемого кода. В частности, в скомпилированной программе вызовы hu
должны напрямую заменяться вызовами соответствующей C-функции.
Предположим, универсальный интерфейс предоставляет константы времени компиляции. В C это может выглядеть так:
#define needed(X)
_Generic((X),
float: 37,
double: 51)
А в C++ можно задействовать похожий механизм, в котором вместо inline
используется constexpr
:
template constexpr auto needed(T x);
template constexpr auto needed(float x) { return 37; }
template constexpr auto needed(double x) { return 51; }
Макросы
Препроцессоры для C и C++ в наши дни должны выдавать эквивалентные варианты замены макросов соответствующим кодом. Цель тех, кто занимается стандартизацией C и C++, заключается в том, чтобы поддерживать их в полностью согласованном состоянии. Мы уже видели несколько примеров, когда препроцессор, при объявлении общих интерфейсов, приходит нам на помощь.
В частности, сравнительно недавно препроцессор C++ был оснащён макросами variadic
. Это — макросы, которые могут принимать разное количество аргументов.
В заключение давайте рассмотрим более сложный пример вариадического интерфейса с одним и тем же типом для всех параметров. Представим, что у нас есть простая функция, которая принимает массив значений типа double
:
size_t median_vec(size_t len, double arr[]);
Смысл тут в том, что мы хотим дать обоим языкам один и тот же интерфейс, позволяющий принимать фиксированный список значений, создающий массив и передающий его функции.
# define ASIZE(...) /* механизм макроса, определяющий длину списка аргументов */
#ifndef __cplusplus
# define ARRAY(T, ...) ((T const[]){ __VA_ARGS__ }) // составной литерал
#else
# define ARRAY(T, ...) (std::initializer_list({ __VA_ARGS__ }).begin()) // стандартный инициализатор массива
#endif
#define median(...) median_vec(ASIZE(__VA_ARGS__), ARRAY(double, __VA_ARGS__))
...
size_t med = median(0, 7, a, 33, b, c);
Приходилось ли вам налаживать взаимодействие между кодом, который написан на C и на C++?
Автор: ru_vds