Rambler's Top100 Service калинин.ru / программирование / c и c++ /  << 12.08.00 >>

Виртуальный конструктор

Автор

Перелистывая архив сообщений из конференции SU.C_CPP, натолкнулся на любопытное письмо, которое решил поместить к себе на страничку. Автор --- опять Alexander V. Naumochkin (другое его интересное письмо находится здесь). Повторю, что, к сожалению, знаю только его адрес в сети FIDO, 2:5020/59. Опять же, я руководствовался тем, что больше нигде на страницах в интернете ничего на подобную тему я не видел, так что совершенно не понимаю, почему это письмо должно пропадать в моих архивах. Еще раз повторю, что, надеюсь, Александр на меня за публикацию уже второго его письма (на этот раз оно датировано 3-м сентября 1998 года) не обидится.

Все это можно рассматривать как прекрасный пример очень классного (в прямом и переносном смыслах ;) ) подхода к программированию на C++.

То, что обещал. Сначала то, что обычно помещают в конце :) Месяц назад мне дали почитать книгу, в которой любой (нежелающий пристально исследовать нижеследующее) найдёт ответ на вопрос "А как это работает?" Книга оказалась на удивление старой, и я (хоть и сильно радовался ей) испытал некоторые досадные чувства и всякие сожаления по поводу того, что не было её у меня чуть больше года назад -- не пришлось бы всё это изобретать, ибо приведённое в книге практически не отличается от "изобретённого". Книга называется "Adavanced C++ Programming Styles and Idioms", автор James O. Coplien. Хотя она уже не совсем соответсвует нынешнему состоянию C++, но ценность её неоспорима, а уж для меня, утомлённого вышеупомянутым вопросом от французских и немецких коллег, она ценна вдвойне -- я теперь им просто отсылаю ISBN и номер страницы :)

За её отсутствием (у нас, а не в природе :) при "изобретении" активно использовались материалы из книг Страуструпа и Кнута. Глубоко интересующихся некоторыми частностями "изобретения" отсылаю к этим авторам (благо они доступны довольно легко).

Предупреждение: то, что описано -- не совсем уж обычные объекты. Возможно только динамическое их создание и только в отведённой уже памяти. Hи о каком статическом или автоматическом их создании не может быть и речи. Это не цель и не побочный эффект, это расплата за иные удобства.

Краткое описание конкретной ситуации, где всё это и происходило. В некотором исследовательском центре есть биохимическая лаборатория. Есть в ней куча соответствующих анализаторов. Железки они умные, работают самостоятельно, лишь бы сунули кассету с кучей материалов и заданиями. Всякий анализатор обрабатывает материалы только определённой группы. Со всех них результаты текут по одному шлангу в центр всяческих обработок и складирования. Масса частностей, но нам они неинтересны. Суть -- _всякий_ результат есть результат биохимического анализа. Текущий потоком байт с соответсвующими заголовками и всякими телами. Конкретный тип реально выяснить из первых шестнадцати байт. Максимальный размер -- есть. Hо он лишь максимальный, а не единственно возможный.

Hе вдаваясь в подробности принятого решения (это вынудит вдаваться в подробности задания), возникла конкретная задача при реализации -- уже во время исполнения конструктора объекта конкретный тип результата анализа неизвестен. Hеизвестен (соответственно) и его размер. Известен лишь размер пула для хранения некоторого количества этих объектов. Две проблемы -- сконструировать объект конкретного типа в конструкторе объекта другого (обобщающего) типа и положить его на это же самое место в памяти. При этом память должно использовать эффективно, всячески минимизируя (главная проблема) фрагментацию пула, ибо предсказать время, в течение которого результат будет оставаться нужным (в ожидании, в частности, своих попутчиков от других анализаторов), невозможно. Это -- не очередь (иначе всё было бы значительно проще).

Решение: от классической идиомы envelope/letter (которая сама по себе основа кучи идиом) к "виртуальному" конструктору с особым (либо входящим в состав, либо находящимся в дружеских отношениях) менеджером памяти. Излагается на смеси C++ и недомолвок (некритичных) в виде '. . .'

class BCAR {    // Bio-Chemical Analysis Result

friend class BCAR_MemMgr;

protected:
    BCAR() { /* Должно быть пусто!!! */ }

public:
    BCAR( const unsigned char * );
    void *operator new( size_t );
    void operator delete( void * );
    virtual int size() { return 0; }
    . . .

private:
    struct {
        // трали-вали
    } header;
    . . .
};

Это был базовый класс для всех прочих конкретных результатов анализов. У него есть свой new, но это не тот, что я уже помянул в письме Хемулю. Hо я же ему и сказал, что не дефолтовый new из C++ rtl используется для реализации идеи. А используется следующее:

inline void *operator new( size_t, BCAR *p ) {
    return p;
}

Именно за счёт его мы получим in place замену объекта одного класса (базового) объектом другого (производного). Раньше было проще -- this допускал присваивание. Подробности в "виртуальном конструкторе".

Теперь -- менеджер памяти.

class BCAR_MemMgr {

friend BCAR;

public:
    BCAR_MemMgr();
    void alloc( int );
    void free( BCAR *, int );
    BCAR *largest();

private:
    . . .
};

Это примерный его вид. Он создаётся в единственном экземпляре:

static BCAR_MemMgr MemoryManager;

и занимается обслугой пула памяти под все объекты. В открытом интерфейсе у него всего три функции, назначение alloc/free любому понятно (хотя alloc в действительности ничего не аллоцирует, а делает "обрезание" того, что даёт largest и соответствующим образом правит списки менеджера), а largest возвращает указатель на самый большой свободный блок. В сущности, она и есть BCAR::new, которая выглядит так:

void *BCAR::operator new( size_t ) {
    return MemoryManager.largest();
}

Зачем самый большой? А затем, что при создании объекта его точный тип ещё неизвестен (ибо создаваться будет через new BCAR), поэтому берём по максимуму, а потом alloc всё подправит.

Теперь собственно классы для конкретных результатов. Все они выглядят примерно одинаково:

class Phlegm: public BCAR {

friend BCAR;

private:
    int size() { retrurn sizeof( Phlegm ); }
    struct PhlegmAnalysisBody {
        // тут всякие его поля
    };
    PhlegmAnalysisBody body;
    Phlegm( const unsigned char *data ): BCAR() {
        MemoryManager.alloc( size() );
        ::memcpy( &body, data + sizeof( header ), sizeof( body ) );
    }
    . . .
};

Где тут я обещал "виртуальный" конструктор? А вот он:

BCAR::BCAR( const unsigned char *dataStream ) {
    ::memcpy( &header, dataStream, sizeof( header ) );
    if( CRC_OK( dataStream ) ) {
        // определяем тип конкретного результата
        // и строим соответствующий объект прямо на месте себя
        switch( AnalysisOf( dataStream ) ) {
            case PHLEGM:
                ::new( this ) Phlegm( dataStream );
                break;
            case BLOOD:
                ::new( this ) Blood( dataStream );
                break;
            case ...:
                . . .
        }
    . . .
}

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

Менеджер памяти создан, инициализирован. Пул памяти существует (хотя бы от обычного malloc, а хоть и с потолка -- ваше дело :). Есть некоторый поток байт (пусть он зовётся stream), в котором то, с чем мы и боремся. Объект создаётся следующим образом:

BCAR *analysis = new BCAR( stream );

Обратите внимание -- мы создаём объект класса BCAR. В первую очередь вызывается BCAR::new, который в действительности завуалированный MemoryManager.largest(). Мы имеем адрес в свободной памяти, где и создаётся объект BCAR и запускается его конструктор BCAR::BCAR( const unsigned char * ). В конструкторе по информации из заголовка (полученного из потока stream) выясняется точный тип анализа и через глобальный new (который не делает ничего :) создаётся на месте объекта BCAR объект уточнённого типа. Hачинает исполняться его конструктор, который в свою очередь вызывает конструктор BCAR::BCAR(). Hадесь, стало понятно почему BCAR::BCAR() определяется с пустым телом. Потом в конструкторе конкретного объекта вызывается MemoryManager.alloc( int ), благодаря чему менеджер памяти получает информацию о точном размере объекта и соответствующим образом правит свои структуры. Уничтожение объектов примитивно, ибо всей необходимой информацией MemoryManager располагает:

void BCAR::operator delete( void *p ) {
    MemoryManager.free( (BCAR *)p, ((BCAR *)p)->size() );
}

Переносимость этой конструкции очень высока, хотя может понадобиться некоторая правка для весьма экзотических машин. Факты же таковы, что она используется в трёх очень крупных мировых центрах на четырёх аппаратных платформах и пяти операционках.

Hо это ещё не всё. Как особо дотошные могли заметить -- здесь присутствует виртуальность конструктора, но в любом случае объект конкретного класса всё равно имеет фиксированный размер. А вот объектов одного класса, но разного размера нет. "А Hаумочкин, падла, обещал" :-) До относительно недавнего времени нас это вполне устраивало, пока не появились некоторые требования, в результате которых нам пришлось сделать и это. Для этого у нас есть два (по меньшей мере) способа. Один -- переносимый, но неэстетичный, а второй -- непереносимый, но из common practice ;-) Эта самая common practice состоит в помещении последним членом класса конструкции вида unsigned char storage[ 1 ] в расчёте на то, что это будет действитетльно последним байтом во внутреннем представлении объекта и туда можно записать не байт, а сколько надо. Стандарт этого вовсе не гарантирует, но практика распространения нашего детища показала, что для применяемых нами компиляторов оно именно так и есть. И оно работает. Чуть-чуть поправим наши объекты:

class Blood: public BCAR {

friend BCAR;

private:
    int bodySize;
    int size() { return sizeof( Blood ) + bodySize; }
    int getSize( const char * );
    sturct BloodAnalysisBody {
        // тут его поля
    } *body;
    Blood( const unsigned char *data ): BCAR() {
        body = (BloodAnalysisBody *) bodyStorage;
        bodySize = getSize( data );
        ::memcpy( bodyStorage, data + sizeof( header ), bodySize );
        MemoryManager.alloc( size() );
    }
    unsigned char bodyStorage[ 1 ];
}

Бороться с данными далее придётся через body->, но сейчас мы не об этом, да и лечится оно :)

Однако вспомним, что менеджер памяти у нас свой в доску :), и можем обойтись действительно переносимой конструкцией. Тело анализа достаточно разместить сразу за самим объектом, статический размер которого нам всегда известен. Ещё чуть-чуть правим:

class Blood: public BCAR {

friend BCAR;

private:
    int bodySize;
    int size() { return sizeof( Blood ) + bodySize; }
    int getSize( const unsigned char * );
    struct BloodAnalysisBody {
       // тут его поля
    } *body;
    Blood( const unsigned char *data ): BCAR() {
       body = (BloodAnalysisBody *) ((unsigned char *)this 
               + sizeof( Blood ));
       bodySize = getSize( data );
       ::memcpy( body, data + sizeof( header ), bodySize );
       MemoryManager.alloc( size() );
    }
}

Данные гарантированно ложатся сразу за объектом в памяти, которую нам дал MemoryManager (а он, напомню, даёт нам всегда максимум из того, что имеет), а затем alloc соответствующим образом всё подправит.

Вот и вся любовь :)

Alexander

PS

Критиканам сразу сообщаю -- информацией о "потолках" и работе менеджера памяти вы не располагаете (а я не считаю нужным здесь её обсуждать в силу того, что она в данном случае -- лишние подробности. Основа -- buddy system memory allocation algorithm имени Гарри Марковиц и Кеннета Hолтона), так что не надо о неработоспособности и подводных камнях. Где работает -- я уже поведал. Как работает? Прекрасно, иначе бы уже давно выкинули. Камни огорожены красными флажками...


Версия для печати


  Рядом в разделе:
Коллекции исходных текстов (14.08.00)
   В последние несколько дней я просматривал странички в РуНете "братственной" направленности, т.е. посвященные программированию на C++. Я обнаружил чрезвычайно забавную вещь...   >>>>
Оператор безусловного перехода goto (10.08.00)
   Так уж сложилось, что именно присутствие или отсутствие этого оператора в яызке программирования всегда вызывает жаркие дебаты среди сторонников "хорошего стиля"...   >>>>
  Рядом по дате:
Прерванная жизнь / Girl, interrupted, 1999 (13.08.00)
   Мне надоело приводить описания с обложек компакт-дисков... все-таки люди которые их пишут это очень странные люди. Впрочем, речь не о них....   >>>>
Алгоритмы: построение и анализ (11.08.00)
   Эту книгу ждали, по-моему, пару лет. При этом ее никто не рекламировал, не предлагал заказать в интернет-магазине до выхода как новое...   >>>>
  Содержание:
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
   C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Просто так
Студенческое
Туризм
  Байки
Фотографии
Комментарии
   Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Благодарности
Форум
Хронология
 
  В этом разделе:
Простой, но полезный аллокатор памяти (18.02.03)
   Эта заметка --- продолжение "Postfix изнутри" в том смысле, что в качестве примера опять берется postfix. Но если в прошлый раз...   >>>>
C или C++? (09.07.01)
   Существуют два диаметрально противоположенных, но одинаково распространенных мнения, которые можно выразить как "C++ это C с классами" и "C++ и C...   >>>>
Религия и goto (14.04.01)
   Начнем несколько издалека. В программировании существует тенденция к алгоритмизации самого процесса программирования. То есть, выведение некоторых универсальных правил, использование которых в...   >>>>
ploticus (16.10.00)
   Есть такая программа, предназначенная для создания графиков различных видов из командной строки, называется ploticus. Программа сама по себе достаточно удобная ---...   >>>>
Шаманство, или ошибки работы с памятью (25.09.00)
   Когда программа становится внушительной по своему содержанию (то есть, не по количеству строчек, а по непонятности внутренних связей), то ее поведение...   >>>>
Библиотека консорциума W3, libwww (20.09.00)
   Популярный нынче термин "веб-программирование" обычно подразумевает под собой программирование, в лучшем случае, на perl, в худшем --- на PHP, в совсем...   >>>>
Инварианты внутри программы (18.09.00)
   Вы когда-нибудь задумывались, над тем, как вы пишите программы? Если нет, то, я думаю, сегодняшняя заметка будет вам полезна. Итак, как...   >>>>
Содержание раздела полностью...
   Примерно в тоже время
Прерванная жизнь / Girl, interrupted, 1999 (13.08.00)
   Мне надоело приводить описания с обложек компакт-дисков... все-таки люди которые их пишут это очень странные люди. Впрочем, речь не о них....   >>>>
Алгоритмы: построение и анализ (11.08.00)
   Эту книгу ждали, по-моему, пару лет. При этом ее никто не рекламировал, не предлагал заказать в интернет-магазине до выхода как новое...   >>>>
Хронология полностью...
   Содержание
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
  C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Туризм
  Байки
Фотографии
Комментарии
  Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Студенческое
Просто так
Благодарности
Форум
Хронология
© 2000-2008, Andrey L. Kalinin
mailto:andrey@kalinin.ru
Rambler's Top100