2. C и C++


2.1. Глобальное пространство имен

В языке C не существует структурированного пространства имен. Спрятать глобальную переменную или функцию можно только при помощи ключевого слова static в пределах одного модуля. Однако в C++ ситуация изменилась, внутри определения класса можно объявлять подтипы, функции (методы) и данные. Классы имеют разграничение на права доступа - private, public и protected. public - доступ открыт для всех. private - доступ открыт только для friend'ов и членов самого класса. protected - доступ открыт для friend'ов, членов самого класса и членов всех классов-потомков. Такой подход позволяет создать пользовательский интерфейс классу и закрыть внутренние данные, другими словами в C++ возможна инкапсуляция данных. Это позволяет структурировать пространство имен, сократить число коллизий, увеличить читабельность и гибкость кода. Приведу пример определения подтипов:

...
class AVLTree
{
    ...
    typedef (*Callback)(void *param1, void *data);
    enum NodeState { State1, State2, State3 };
    ...
};
...
В C++ не стоит делать так:
...
typedef (*AVLCallback)(void *param1, void *data);
enum AVLNodeState { State1, State2, State3 };
class AVLTree
{
...
};
...
Во втором случае State1, State2, State3 повлияют на глобальное пространство имен. Еще хуже в случае с использованием препроцессора:
...
#define State1 1
#define State2 2
#define State3 3
...
В случае с использованием препроцессора при отладке программы будет невозможно увидеть имя типа и значение переменной в текстовом виде . Также во втором и третьем случае легко получить коллизию по именам, причем, если во втором компилятор укажет ошибку корректно, то в третьем это не обязательно. Представьте себе ситуацию, что в одном заголовке было глобальное описание StateX define'ами, а в другом enum'ом. Такая коллизия, например, имеется между библиотеками Qt и Xlib. Имя "GrayScale" в Qt описано по всем правилам внутри класса, а в заголовках Xlib "GrayScale" это глобальный define. enum является частью грамматики компилятора, поэтому идентификаторы неанонимного enum'а попадают в отладочную информацию. define заменяются на цифровые значения на стадии препроцессинга.

Вывод: надо заворачивать все свободные имена из глобального пространства имен в классы. Если есть совсем несвязываемая информация - используйте класс Global. Например:

...
int foo(int x);
int bar(int y);
...
привести к виду:
...
class Global
{
public:
    static int foo(int x);
    static int bar(int y);
};
...

Для внешних библиотек так сделать не получится. Если коллизий между используемыми библиотеками не возникает, то не стоит даже тратить время на изменение имен в этих библиотеках. Однако если возникают коллизии по именам, самый простой обходной путь, при наличии исходных текстов библиотек - namespace. Namespace достаточно новое ключевое слово, и еще не поддерживается всеми компиляторами C++. Если не поддерживается namespace или библиотека имеется только в бинарном виде, можно добавить префиксы к именам функций.

Преимущество инкапсулированных объектов
Первый пример класса Point
Второй пример класса Point
Заметьте, что при изменении формата хранения данных, пользовательский интерфейс совсем не изменился. Размер класса point в первом случае 8 байт, во втором 4 байта. А 4-х байтный класс это уже немалое ускорение на 32-битных платформах.

C++ поддерживает полиморфные функции. На первый взгляд вместо:

void getRGB(int index, int &r, int &g, int &b);
void getRGB(int index, unsigned char *vec);
можно написать на C:
void getRGB3(int index, int *r, int *g, int *b);
void getRGBv(int index, unsigned char *vec);
Но на практике намного удобнее первый вариант, поскольку не надо тратить время на придумывание запутанных венгерских нотаций - компилятор сам знает, где какие типы. Полиморфизмом нельзя злоупотреблять. Все полиморфные функции должны выполнять одно и то же действие - в случае getRGB - получить цвет из системной палитры, иначе можно легко запутать разработчиков.

2.2. Накладные расходы C++ компилятора

У многих сложилось мнение, что C++ программа будет намного больше в исполняемом виде, чем аналогичная, но написанная на C и будет медленнее работать. Это неверно. Размер исполняемого файла в основном растет из-за информации исключений (Exceptions Frame Information), RTTI (Run-Time Type Information) и отладочной информации.

Исключения - достаточно большие накладные расходы на каждую функцию программы. Сами-же исключения не предоставляют значительного выигрыша при обработке ошибок. По производительности - исключения гораздо медленнее дерева условий. По совместимости - исключения еще плохо поддерживаются многими компиляторами. Думаю пока стоит отказаться от использования исключений.

С RTTI ситуация полностью аналогичная исключениям. Работает неэффективно, и не оправдывает затраты. Не поддерживается многими компиляторами. Чтобы не использовать RTTI, можно добавить дополнительный член класса - тип объекта. Это обычно целое число, равное сгенерированному уникальному номеру. В некоторых случаях удобнее использовать строковую константу или виртуальную функцию classOf(), которая возвращает тип объекта. Самый быстрый вариант - использовать целый идентификатор типа. Такой подход дополнительно позволяет ограничить количество классов, несущих информацию о своем типе.

Отладочная информация. C++ достаточно сложен и генерирует огромные по сравнению с C, объемы debug info. Об этом волноваться не стоит, в релизе ее все равно не будет.

Виртуальные методы. Для разъяснения привожу программу на C++ и полный аналог на C. Легко можно понять, какие в действительности идут накладные расходы.
На C++:

class X
{
    virtual void f();
};
....
X *p;
....
p->f();
....
На C:
struct X_virtual_table
{
    void (*f)(struct X *this);
};
struct X
{
    struct S_virtual_table *_vt;
};
....
struct X *p;
....
(*p->_vt->f)(p);
....

2.3. Отличие роли заголовков в C и C++

В C в заголовках находились только объявления переменных, функций и определения типов. В C++ ситуация усложнилась, в заголовках оказались инлайновые функции и шаблоны. Плюс дополнительная документация, и получаем, что объемы C++ заголовков намного больше C-шных. И это приводит к проблеме. Когда в модуле включается слишком много файлов-заголовков, скорость компиляции сильно падает.

2.4. Зависимости

При разработке компонент, обычно будет происходить так, что одна компонента зависит от другой (через наследование, композицию, использование каких-либо методов, данных, типов ...). Могут возникать циклические зависимости - они разрешаются специальными приемами, о которых будет сказано позднее. По примеру простого заголовка point.h видно, что будет в реальном проекте, где средний размер заголовка не 1kb, а 10kb, и их не менее 50 штук. В C++ появляется новая задача, которая, естественно, тоже вызвала отвращение у программистов - минимизация зависимостей между заголовками. Хорошо, что C++ позволяет это делать без заметных накладных расходов. Как это сделать будет рассказано далее.


Валерий Щедрин <valery@forthpick.kiev.ua>

1