Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Языки программирования С, С++

.pdf
Скачиваний:
136
Добавлен:
01.05.2014
Размер:
1.43 Mб
Скачать

Трюки программирования

компилятора при включении заголовочных файлов, но, поверьте, он существует.

Отладка программы, содержащую подобную ошибку, оказывается проверкой на устойчивость психики. Потому что выглядит это примерно так: следим за объектом, за его полями, все выглядит просто замечательно и вдруг, после того как управление передается какой то функции, все, что содержится в объекте, принимает «бредовые» формы, какие то неизвестно откуда взявшиеся цифры...

На самом деле #pragma pack не является панацеей. Мало того, использование этой директивы практически всегда неправомерно. Можно даже сказать, что эта директива в принципе редко когда нужна (во всяком случае, при прикладном программировании).

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

template<class T>

inline size_t get_size(const T& obj)

{

return sizeof(obj);

}

Эта функция возвращает размер, необходимый для записи объекта. Зачем она понадобилась? Во первых, возможен вариант, что sizeof возвращает размер не в байтах, а в каких то собственных единицах. Во вторых, и это значительно более необходимо, объекты, для которых вычисляется размер, могут быть не настолько простыми, как int. Например:

template<>

inline size_t get_size<std::string>(const std::string& s)

{

return s.length() + 1;

}

Надеемся, понятно, почему выше нельзя было использовать sizeof.

397

Трюки программирования

Аналогичным образом определяются функции, сохраняющие в буфер данные и извлекающие из буфера информацию:

typedef unsigned char byte_t;

template<class

T>

 

 

inline size_t

save(const

T&

i, byte_t* buf)

{

 

 

 

*((T*)buf) = i;

 

 

return get_size(i);

 

 

}

 

 

 

template<class

T>

 

 

inline size_t

restore(T&

i,

const byte_t* buf)

{

 

 

 

i = *((T*)buf); return get_size(i);

}

Понятно, что это работает только для простых типов (int или float), уж очень много чего наворочено: явное приведение указателя к другому типу, оператор присваивания... конечно же, очень нехорошо, что такой save() доступен для всех объектов. Понятно, что очень просто от него избавиться, убрав шаблонность функции и реализовав аналогичный save() для каждого из простых типов данных. Тем не менее, это всего лишь примеры использования:

template<>

inline size_t save<MyObject>(const MyObject& s, byte_t* buf)

{

// ...

}

Можно сделать и по другому. Например, ввести методы save() и restore() в каждый из сохраняемых классов, но это не столь важно для принципа этой схемы. Поверьте, это достаточно просто использовать, надо только попробовать. Мало того, здесь можно вставить в save<long>() вызов htonl() и в restore<long>()

вызов ntohl(), после чего сразу же упрощается перенос двоичных файлов на платформы с другим порядком байтов в слове... в общем, преимуществ — море. Перечислять все из них не стоит, но как после этого лучше выглядит исходный текст, а как приятно вносить изменения…

398

Трюки программирования

Оператор безусловного перехода goto

Так уж сложилось, что именно присутствие или отсутствие этого оператора в языке программирования всегда вызывает жаркие дебаты среди сторонников «хорошего стиля» программирования. При этом, и те, кто «за», и те, кто «против» всегда считают признаком «хорошего тона» именно использование goto или, наоборот, его неиспользование. Не вставая на сторону ни одной из этих «школ», просто покажем, что действительно есть места, где использование goto выглядит вполне логично.

Но сначала о грустном. Обычно в вину goto ставится то, что его присутствие в языке программирования позволяет делать примерно такие вещи:

int i,

j;

 

 

 

for(i =

0;

i

<

10; i++)

{

 

 

 

 

// ...

 

 

 

if(condition1)

{

 

 

 

 

j =

4;

 

 

goto label1;

}

 

 

 

 

// ...

 

 

 

for(j

=

0;

j

< 10; j++)

{

 

 

 

 

//...

label1:

//...

if(condition2)

{

i = 6;

goto label2;

}

}

// ...

399

Трюки программирования

label2: // ...

}

Прямо скажем, что такое использование goto несколько раздражает, потому что понять при этом, как работает программа при ее чтении будет очень сложно. А для человека, который не является ее автором, так и вообще невозможно. Понятно, что вполне вероятны случаи, когда такого подхода требует какая нибудь очень серьезная оптимизация работы программы, но делать что то подобное программист в здравом уме не должен. На самом деле, раз уж мы привели подобный пример, в нем есть еще один замечательный нюанс — изменение значения переменной цикла внутри цикла. Смеем вас заверить, что такое поведение вполне допустимо внутри do или while; но когда используется for — такого надо избегать, потому что отличительная черта for как раз и есть жестко определенное местоположение инициализации, проверки условия и инкремента (т.е., изменения переменной цикла). Поэтому читатель исходного текста, увидев «полный» for (т.е. такой, в котором заполнены все эти три места) может и не заметить изменения переменной где то внутри цикла. Хотя для циклов с небольшим телом это, наверное, все таки допустимо — такая практика обычно применяется при обработке строк (когда надо, например, считать какой то символ, который идет за «спецсимволом», как «\\» в строках на Си; вместо того, чтобы вводить дополнительный флаг, значительно проще, увидев «\», сразу же сдвинуться на одну позицию и посмотреть, что находится там). В общем, всегда надо руководствоваться здравым смыслом и читабельностью программы.

Если здравый смысл по каким то причинам становится в противовес читабельности программы, то это место надо обнести красными флагами, чтобы читатель сразу видел подстерегающие его опасности.

Тем не менее, вернемся к goto. Несмотря на то, что такое расположение операторов безусловного перехода несколько нелогично (все таки, вход внутрь тела цикла это, конечно же, неправильно) — это встречается.

Итак, противники использования goto в конечном итоге приходят к подобным примерам и говорят о том, что раз такое его

400

Трюки программирования

использование возможно, то лучше чтобы его совсем не было. При этом, конечно же, никто обычно не спорит против применения, например, break, потому что его действие жестко ограничено. Хочется сказать, что подобную ситуацию тоже можно довести до абсурда, потому что имеются программы, в которых введен цикл только для того, чтобы внутри его тела использовать break для выхода из него (т.е., цикл делал только одну итерацию, просто в зависимости от исходного состояния заканчивался в разных местах). И что помешало автору использовать goto (раз уж хотелось), кроме догматических соображений, не понятно.

Собственно, мы как раз подошли к тому, что обычно называется «разумным» применением этого оператора. Вот пример:

switch(key1)

{

case q1 : switch(key2)

{

case q2 : break;

}

break;

}

Все упрощено до предела, но, в принципе, намек понятен. Есть ситуации, когда нужно что то в духе break, но на несколько окружающих циклов или операторов switch, а break завершает только один. Понятно, что в этом примере читабельность, наверное, не нарушена (в смысле, использовался бы вместо внутреннего break goto или нет), единственное, что в таком случае будет выполнено два оператора перехода вместо одного (break это, все таки, разновидность goto).

Значительно более показателен другой пример: bool end_needed = false;

for( ... )

{

for( ... )

{

if(cond1) { end_needed = true; break; }

}

401

Трюки программирования

if(end_needed) break;

}

Т.е., вместо того, чтобы использовать goto и выйти из обоих циклов сразу, пришлось завести еще одну переменную и еще одну проверку условия. Тут хочется сказать, что goto в такой ситуации выглядит много лучше — сразу видно, что происходит; а то в этом случае придется пройти по всем условиями и посмотреть, куда они выведут. Надо сказать (раз уж мы начали приводить примеры из жизни), что не раз можно видеть эту ситуацию, доведенную до крайности — четыре вложенных цикла (ну что поделать) и позарез надо инициировать выход из самого внутреннего. И что? Три лишних проверки... Кроме того, введение еще одной переменной, конечно же, дает возможность еще раз где нибудь допустить ошибку, например, в ее инициализации. Опять же, читателю исходного текста придется постоянно лазить по тексту и смотреть, зачем была нужна эта переменная... в общем: не плодите сущностей без надобности. Это только запутает.

Другой пример разумного использования goto следующий: int foo()

{

int res;

//...

if(...)

{

res = 10; goto finish;

}

//...

finish: return res;

}

Понятно, что без goto это выглядело бы как return 10 внутри if. Итак, в чем преимущества такого подхода. Ну, сразу же надо вспомнить про концептуальность — у функции становится только один «выход», вместо нескольких (быстро вспоминаем про IDEF). Правда, концептуальность — это вещь такая...

Неиспользование goto тоже в своем роде концептуальность, так что это не показатель (нельзя противопоставлять догму догме, это

402

Трюки программирования

просто глупо). Тем не менее, выгоды у такого подхода есть. Во первых, вполне вероятно, что перед возвратом из функции придется сделать какие то телодвижения (закрыть открытый файл, например). При этом, вполне вероятно, что когда эта функция писалась, этого и не требовалось — просто потом пришлось дополнить. И что? Если операторов return много, то перед каждым из них появится одинаковый кусочек кода. Как это делается? Правильно, методом «cut&paste». А если потом придется поменять? Тоже верно, «search&replace». Объяснять, почему это неудобно не будем — это надо принять как данность.

Во вторых, обработка ошибок, которая также требует немедленного выхода с возвратом используемых ресурсов. В принципе, в C++ для этого есть механизм исключительных ситуаций, но когда он отсутствует (просто выключен для повышения производительности), это будет работать не хуже. А может и лучше по причине более высокой скорости.

В третьих, упрощается процесс отладки. Всегда можно проверить что возвращает функция, поставить точку останова (хотя сейчас некоторые отладчики дают возможность найти все return и поставить на них точки останова), выставить дополнительный assert или еще что нибудь в этом духе. В общем, удобно.

Еще goto очень успешно применятся при автоматическом создании кода — читателя исходного текста там не будет, он будет изучать то, по чему исходный текст был создан, поэтому можно (и нужно) допускать различные вольности.

В заключение скажем, что при правильном использовании оператор goto очень полезен. Надо только соблюдать здравый смысл, но это общая рекомендация к программированию на С/С++ (да и вообще, на любом языке программирования), поэтому непонятно почему goto надо исключать.

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

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

403

Трюки программирования

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

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

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

class BCAR { // Bio Chemical Analysis Result

friend class BCAR_MemMgr;

protected:

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

404

Трюки программирования

public:

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

. . .

private: struct {

//трали вали

}header;

. . .

};

Это был базовый класс для всех прочих конкретных результатов анализов. У него есть свой new. Но для реализации идеи используется не дефолтовый 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:

. . .

};

405

Трюки программирования

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

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 ) );

406