Виртуальный конструктор
То, что обещал. Сначала то, что обычно помещают в конце :) Месяц назад мне дали почитать книгу, в которой любой (нежелающий пристально исследовать нижеследующее) найдёт ответ на вопрос "А как это работает?" Книга оказалась на удивление старой, и я (хоть и сильно радовался ей) испытал некоторые досадные чувства и всякие сожаления по поводу того, что не было её у меня чуть больше года назад -- не пришлось бы всё это изобретать, ибо приведённое в книге практически не отличается от "изобретённого". Книга называется "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;
и занимается обслугой пула памяти под все объекты. В открытом интерфейсе у
него всего три функции, назначение
void *BCAR::operator new( size_t ) {
return MemoryManager.largest();
}
Зачем самый большой? А затем, что при создании объекта его точный тип ещё
неизвестен (ибо создаваться будет через Теперь собственно классы для конкретных результатов. Все они выглядят примерно одинаково:
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 ...:
. . .
}
. . .
}
Теперь, чтобы не вынуждать вас носиться по всему письму в целях построения целостной картины, объясняю происходящее по шагам.
Менеджер памяти создан, инициализирован. Пул памяти существует (хотя бы от
обычного BCAR *analysis = new BCAR( stream );
Обратите внимание -- мы создаём объект класса
void BCAR::operator delete( void *p ) {
MemoryManager.free( (BCAR *)p, ((BCAR *)p)->size() );
}
Переносимость этой конструкции очень высока, хотя может понадобиться некоторая правка для весьма экзотических машин. Факты же таковы, что она используется в трёх очень крупных мировых центрах на четырёх аппаратных платформах и пяти операционках.
Hо это ещё не всё. Как особо дотошные могли заметить -- здесь присутствует
виртуальность конструктора, но в любом случае объект конкретного класса всё
равно имеет фиксированный размер. А вот объектов одного класса, но разного
размера нет. "А Hаумочкин, падла, обещал" :-) До относительно недавнего
времени нас это вполне устраивало, пока не появились некоторые требования, в
результате которых нам пришлось сделать и это. Для этого у нас есть два (по
меньшей мере) способа. Один -- переносимый, но неэстетичный, а второй --
непереносимый, но из common practice ;-) Эта самая common practice состоит в
помещении последним членом класса конструкции вида
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 ];
}
Бороться с данными далее придётся через Однако вспомним, что менеджер памяти у нас свой в доску :), и можем обойтись действительно переносимой конструкцией. Тело анализа достаточно разместить сразу за самим объектом, статический размер которого нам всегда известен. Ещё чуть-чуть правим:
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() );
}
}
Данные гарантированно ложатся сразу за объектом в памяти, которую нам дал
Вот и вся любовь :) Alexander PSКритиканам сразу сообщаю -- информацией о "потолках" и работе менеджера памяти вы не располагаете (а я не считаю нужным здесь её обсуждать в силу того, что она в данном случае -- лишние подробности. Основа -- buddy system memory allocation algorithm имени Гарри Марковиц и Кеннета Hолтона), так что не надо о неработоспособности и подводных камнях. Где работает -- я уже поведал. Как работает? Прекрасно, иначе бы уже давно выкинули. Камни огорожены красными флажками...
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
© 2000-2008, Andrey L. Kalinin mailto:andrey@kalinin.ru |
|