мя параметрами. Первый параметр -- это имя (большого) файла для чтения, а второй -- цифра 1, 2 или 3, выбирающая функцию workc(), workcpp() или work3() соответственно. Только не забудьте про дисковый кэш, т.е. для получения объективных результатов программу нужно запустить несколько раз для каждого из вариантов.

Необычным местом здесь является функция work3() и соответствующий ей класс File. Они написаны специально для проверки "честности" реализации стандартных средств ввода-вывода C -- FILE*. Если вдруг окажется, что workc() работает существенно медленнее work3(), то вы имеете полное право назвать создателей такой библиотеки, как минимум, полными неучами.

А сейчас попробуем получить информацию к размышлению: проведем серию контрольных запусков и посмотрим на результат.

И что же нам говорят безжалостные цифры? Разница в разы! А для одного широко распространенного коммерческого пакета (не будем показывать пальцем) она порой достигала 11 раз!!!

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

Для C с его getc() в типичной реализации мы имеем:

#define getc(f) ((--((f)->level) >= 0) ? (unsigned char)(*(f)->curp++) : _fgetc (f))
Т.е. коротенький макрос вместо функции. Как говорится -- всего-ничего. А вот для C++ стандарт требует столько, что очередной раз задаешься вопросом: думали ли господа-комитетчики о том, что горькие плоды их творчества кому-то реально придется применять?!

Ну и ладно: предупрежден -- вооружен! А что, если задать буфер побольше?

void workc(char* fn)
{
 // ...

 if (setvbuf(fil, 0, _IOFBF, LARGE_BUFSIZ)) return;

 // ...
}

void workcpp(char* fn)
{
 // ...

 char* buf=new char[LARGE_BUFSIZ];
 fil.rdbuf()->pubsetbuf(buf, LARGE_BUFSIZ);

 // ...

 delete [] buf;
}
Как ни странно, по сути ничего не изменится! Дело в том, что современные ОС при работе с диском используют очень качественные алгоритмы кэширования, так что еще один уровень буферизации внутри приложения оказывается излишним (в том смысле, что используемые по умолчанию буферы потоков вполне адекватны).

Кстати, одним из хороших примеров необходимости использования многопоточных программ является возможность ускорения работы программ копирования файлов, когда исходный файл и копия расположены на разных устройствах. В этом случае программа запускает несколько потоков, осуществляющих асинхронные чтение и запись. Но в современных ОС в этом нет никакого смысла, т.к. предоставляемое системой кэширование кроме всего прочего обеспечивает и прозрачное для прикладных программ асинхронное чтение и запись.

Подводя итог, хочется отметить, что если ввод-вывод является узким местом вашего приложения, то следует воздержаться от использования стандартных потоков C++ и использовать проверенные десятилетиями методы.


Стр.701: 21.4.6.3. Манипуляторы, определяемые пользователем

Коль скоро с эффективностью потоков ввода-вывода мы уже разобрались, следует поговорить об удобстве. К сожалению, для сколько-нибудь сложного форматирования предоставляемые потоками средства не предназначены. Не в том смысле, что средств нет, а в том, что они чрезвычайно неудобны и легко выводят из себя привыкшего к элегантному формату ...printf() программиста. Не верите? Давайте попробуем вывести обыкновенную дату в формате dd.mm.yyyy:
int day= 31,
    mon= 1,
    year=1974;

printf("%02d.%02d.%d\n", day, mon, year);  // 31.01.1974

cout<<setfill('0')<<setw(2)<<day<<'.'<<setw(2)<<mon<<setfill(' ')<<'.'
    <<year<<"\n";  // тоже 31.01.1974
Думаю, что комментарии излишни.

За что же не любят потоки C и чем потоки C++ могут быть удобнее? У потоков C++ есть только одно существенное достоинство -- типобезопасность. Т.к. потоки C++ все же нужно использовать, я написал специальный манипулятор, который, оставаясь типобезопасным, позволяет использовать формат ...printf(). Он не вызывает существенных накладных расходов и с его помощью приведенный выше пример будет выглядеть следующим образом:

cout<<c_form(day,"02")<<'.'<<c_form(mon,"02")<<'.'<<year<<'\n';
Вот исходный код заголовочного файла:
#include <ostream>

/** личное пространство имен функции c_form, содержащее детали реализации */
namespace c_form_private {

 typedef std::ios_base::fmtflags fmtflags;
 typedef std::ostream ostream;
 typedef std::ios_base ios;

 /**
  * Вспомогательный класс для осуществления форматирования.
  */
 class Formatter {
       /** флаги для установки */
       fmtflags newFlags;
       /** ширина */
       int width;
       /** точность */
       int prec;
       /** символ-заполнитель */
       char fill;
       /** сохраняемые флаги */
       fmtflags oldFlags;

  public:
       /**
        * Создает объект, использующий переданное форматирование.
        */
       Formatter(const char* form, int arg1, int arg2);

       /**
        * Устанавливает новое форматирование для переданного потока, сохраняя
        * старое.
        */
       void setFormatting(ostream& os);

       /**
        * Восстанавливает первоначальное форматирование, сохраненное в функции
        * setFormatting().
        */
       void restoreFormatting(ostream& os);
 };

 /**
  * Вспомогательный класс.
  */
 template <class T>
 class Helper {
       /** выводимое значение */
       const T& val;
       /** объект для форматирования */
       mutable Formatter fmtr;

  public:
       /**
        * Создает объект по переданным параметрам.
        */
       Helper(const T& val_, const char* form, int arg1, int arg2) :
         val(val_), fmtr(form, arg1, arg2) {}

       /**
        * Функция для вывода в поток сохраненного значения в заданном формате.
        */
       void putTo(ostream& os) const;
 };

 template <class T>
 void Helper<T>::putTo(ostream& os) const
 {
  fmtr.setFormatting(os);
  os<<val;
  fmtr.restoreFormatting(os);
 }

 /**
  * Оператор для вывода объектов Helper в поток.
  */
 template <class T>
 inline ostream& operator<<(ostream& os, const Helper<T>& h)
 {
  h.putTo(os);
  return os;
 }
}

/**
 * Функция-манипулятор, возвращающая объект вспомогательного класса, для
 * которого переопределен оператор вывода в ostream. Переопределенный оператор
 * вывода осуществляет форматирование при выводе значения.
 * @param val значение для вывода
 * @param form формат вывода: [-|0] [число|*] [.(число|*)] [e|f|g|o|x]
 * @param arg1 необязательный аргумент, задающий ширину или точность.
 * @param arg2 необязательный аргумент, задающий точность.
 * @throws std::invalid_argument если передан аргумент form некорректного
 *         формата.
 */
template <class T>
inline c_form_private::Helper<T> c_form(const T& val, const char* form,
  int arg1=0, int arg2=0)
{
 return c_form_private::Helper<T>(val, form, arg1, arg2);
}
и файла-реализации:
#include "c_form.hpp"
#include <stdexcept>
#include <cctype>

namespace {

 /**
  * Вспомогательная функция для чтения десятичного числа.
  */
 int getval(const char*& iptr)
 {
  int ret=0;
  do ret=ret*10 + *iptr-'0';
     while (std::isdigit(*++iptr));

  return ret;
 }

}

c_form_private::Formatter::Formatter(const char* form, int arg1, int arg2) :
  newFlags(fmtflags()), width(0), prec(0), fill(0)
{
 const char* iptr=form;  // текущий символ строки формата

 if (*iptr=='-') {  // выравнивание влево
    newFlags|=ios::left;
    iptr++;
 }
 else if (*iptr=='0') {  // добавляем '0'ли только если !left
         fill='0';
         iptr++;
      }

 if (*iptr=='*') {  // читаем ширину, если есть
    width=arg1;
    iptr++;

    arg1=arg2;  // сдвигаем агрументы влево
 }
 else if (std::isdigit(*iptr)) width=getval(iptr);

 if (*iptr=='.') {  // есть точность
    if (*++iptr=='*') {
       prec=arg1;
       iptr++;
    }
    else if (std::isdigit(*iptr)) prec=getval(iptr);
         else throw std::invalid_argument("c_form");
 }

 switch (*iptr++) {
        case   0: return;  // конец строки формата
        case 'e': newFlags|=ios::scientific; break;
        case 'f': newFlags|=ios::fixed;      break;
        case 'g':                            break;
        case 'o': newFlags|=ios::oct;        break;
        case 'x': newFlags|=ios::hex;        break;
        default: throw std::invalid_argument("c_form");
 }

 if (*iptr) throw std::invalid_argument("c_form");
}

void c_form_private::Formatter::setFormatting(ostream& os)
{
 oldFlags=os.flags();
 // очищаем floatfield и устанавливаем свои флаги
 os.flags((oldFlags & ~ios::floatfield) | newFlags);

 if (width) os.width(width);
 if (fill)  fill=os.fill(fill);
 if (prec)  prec=os.precision(prec);
}

void c_form_private::Formatter::restoreFormatting(ostream& os)
{
 os.flags(oldFlags);

 if (fill) os.fill(fill);
 if (prec) os.precision(prec);
}
Принцип его работы основан на следующей идее: функция c_form<>() возвращает объект класса c_form_private::Helper<>, для которого определена операция вывода в ostream.

Для удобства использования, c_form<>() является функцией, т.к. если бы мы сразу использовали конструктор некоторого класса-шаблона c_form<>, то нам пришлось бы явно задавать его параметры:

cout<<c_form<int>(day,"02");
что, мягко говоря, неудобно. Далее. Мы, в принципе, могли бы не использовать нешаблонный класс Formatter, а поместить весь код прямо в Helper<>, но это привело бы к совершенно ненужной повторной генерации общего (не зависящего от параметров шаблона) кода.

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


Стр.711: 21.6.2. Потоки ввода и буфера

Функция readsome() является операцией нижнего уровня, которая позволяет...

Т.к. приведенное в книге описание readsome() туманно, далее следует перевод соответствующей части стандарта:

27.6.1.3 Функции неформатированного ввода [lib.istream.unformatted]

streamsize readsome(char_type* s, streamsize n);
  1. Действия: Если !good() вызывает setstate(failbit), которая может возбудить исключение. Иначе извлекает символы и помещает их в массив, на первый элемент которого указывает s. Если rdbuf()->in_avail() == -1, вызывает setstate(eofbit) (которая может возбудить исключение ios_base::failure (27.4.4.3)) и не извлекает символы;
  2. Возвращает: Количество извлеченных символов.

Стр.773: 23.4.3.1. Этап 1: выявление классов

Например, в математике окружность -- это частный случай эллипса, но в большинстве программ окружность не нужно выводить из эллипса, или делать эллипс потомком окружности.

Думаю, что стоит поподробнее рассмотреть данный конкретный случай, т.к. он иллюстрирует довольно распространенную ошибку проектирования. На первый взгляд может показаться, что идея сделать класс Circle производным от класса Ellipse является вполне приемлемой, ведь они связаны отношением is-a: каждая окружность является эллипсом. Некорректность данной идеи станет очевидной, как только мы приступим к написанию кода.

У эллипса, кроме прочих атрибутов, есть два параметра: полуоси a и b. И производная окружность их унаследует. Более того, нам нужен один единственный радиус для окружности и мы не можем для этих целей использовать один из унаследованных атрибутов, т.к. это изменит его смысл и полученный от эллипса код перестанет работать. Следовательно мы вынуждены добавить новый атрибут -- радиус и, при этом, поддерживать в корректном состоянии унаследованные атрибуты. Очевидно, что подобного рода наследование лишено смысла, т.к. не упрощает, а усложняет разработку.

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


Стр.879: А.5. Выражения

То есть "если нечто можно понять как объявление, это и есть объявление".

Т.к. сложные объявления C++ могут быть непонятны даже неновичку, стоит прокомментировать приведенные в книге объявления. Неочевидность всех приведенных примеров основана на добавлении лишних скобок:

T(*e)(int(3)); эквивалентно T* e(int(3)); То, что инициализация указателя с помощью int запрещена, синтаксичестим анализатором не принимается во внимание: будет распознано объявление указателя и выдана ошибка.
T(f)[4]; эквивалентно T f[4];
T(a); эквивалентно T a;
T(a)=m; эквивалентно T a=m;
T(*b)(); объявление указателя на функцию.
T(x),y,z=7; эквивалентно T x,y,z=7;


Стр.931: B.13.2. Друзья

Приведенный в конце страницы пример нужно заменить на:
template<class C> class Basic_ops {  // базовые операции с контейнерами
	friend bool operator==<>(const C&, const C&);  // сравнение элементов
	friend bool operator!=<>(const C&, const C&);
	// ...
};
Уголки (<>) после имен функций означают, что друзьями являются функции-шаблоны (поздние изменения стандарта).

Этот текст взят из списка авторских исправлений к 10 тиражу.

Почему в данном случае необходимы <>? Потому что иначе мы объявляем другом operator==() не шаблон, т.к. до объявления класса в окружающем контексте не было объявления operator==()-шаблона. Вот формулировка стандарта:

14.5.3. Друзья [temp.friend]

  1. Другом класса или класса-шаблона может быть функция-шаблон, класс-шаблон, их специализации или обычная (не шаблон) функция или класс. Для объявления функций-друзей которые не являются объявлениями шаблонов: Например:
    template<class T> class task;
    template<class T> task<T>* preempt(task<T>*);
    
    template<class T> class task {
    	//  ...
    	friend void next_time();
    	friend void process(task<T>*);
    	friend task<T>* preempt<T>(task<T>*);
    	template<class C> friend int func(C);
    
    	friend class task<int>;
    	template<class P> friend class frd;
    	//  ...
    };
    здесь функция next_time является другом каждой специализации класса-шаблона task; т.к. process не имеет явных template-arguments, каждая специализация класса-шаблона task имеет функцию-друга process соответствующего типа и этот друг не является специализацией функции-шаблона; т.к. друг preempt имеет явный template-argument <T>, каждая специализация класса-шаблона task имеет другом соответствующую специализацию функции-шаблона preempt; и, наконец, каждая специализация класса-шаблона task имеет другом все специализации функции-шаблона func. Аналогично, каждая специализация класса-шаблона task имеет другом класс-специализацию task<int>, и все специализации класса-шаблона frd.

Стр.935: B.13.6. template как квалификатор

И снова об этом загадочном квалификаторе.

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

template <class T> void get_new3();  // (1)

template <class Allocator>
void f(Allocator& m)
{
 int* p1=         m.template get_new1<int>( );
 int* p2=Allocator::template get_new2<int>(m);
 int* p3=                    get_new3<int>(m);
}

struct Alloc {
       template <class T>
       T* get_new1() { return 0; }

       template <class T>
       static T* get_new2(Alloc&) { return 0; }

       template <class T>
       friend T* get_new3(Alloc&) { return 0; }
};

int main()
{
 Alloc a;
 f(a);
}
Итак:
  1. get_new1 --- это функция-член, для вызова которой в данном случае обязательно должен быть использован квалификатор template. Дело в том, что в точке определения f класс Allocator является всего лишь именем параметра шаблона и компилятору нужно подсказать, что данный вызов -- это не (ошибочное) выражение (m.get_new1) < int...
  2. get_new2 -- это статическая функция-член, при вызове из f, ее имя должно быть предварено все тем же квалификатором template по тем же причинам.
  3. А вот get_new3 -- друг класса Alloc, привносит в наш пример некоторые проблемы. Дело в том, что он используется в f до его определения в классе Alloc (точно так же, как я использую до их определения функции get_new1 и get_new2). Чтобы определение f было корректным, мы должны гарантировать, что имя get_new3 известно в точке определения f как имя функции-шаблона. Дабы не ограничивать общность f, я не использовал в точке (1) прототип конкретной get_new3 -- друга класса Alloc, а просто описал (даже не определил!) некоторую функцию-шаблон get_new3. Очевидно, что она не может быть использована в f -- она просто делает вызов
    p3=get_new3<int>(m);
    легальным, внося в область видимости нужное имя-шаблон. Обратите внимание, что описанная в точке (1) функция get_new3 не имеет параметров и не возвращает никакого значения. Это сделано для того, чтобы она не принималась во внимание при выборе подходящей (возможно перегруженной) get_new3, в точке ее вызова в функции f.
Как видите, в случае функции-друга я был вынужден использовать не совсем красивый трюк, т.к. C++ не позволяет мне прямо выразить то, что я хотел, а именно: написать
p3=template get_new3<int>(m);
К сожалению, приходится констатировать, что использование квалификатора template не было в достаточной мере продумано комитетом по стандартизации C++.

Оптимизация

Поговорим об оптимизации.

Что нужно оптимизировать? Когда? И нужно ли вообще? В этих вопросах легко заблудиться, если с самого начала не выбрать правильную точку зрения. Взгляд со стороны пользователя, все сразу ставит на свои места:

  1. Программа должна делать то, что от нее требуется.
  2. Она должна это делать хорошо.
Именно так: глупо оптимизировать неправильно работающий код. Если же пользователя устраивает текущее быстродействие -- не стоит искать неприятности.

Итак, анализ проведен, решение принято -- ускоряемся! Что может ускорить нашу программу? Да все, что угодно; вопрос поставлен некорректно. Что может существенно ускорить нашу программу? А вот над этим уже стоит подумать.

Прежде всего, стоит подумать о "внешнем" ускорении, т.е. о не приводящих к изменению исходного кода действиях. Самый широкораспространенный метод -- использование более мощного "железа". Увы, зачастую это не самый эффективный способ. Как правило, гораздо большего можно добиться путем правильного конфигурирования того, что есть. Например, работа с БД -- практически всегда самое узкое место. Должно быть очевидно, что правильная настройка сервера БД -- это одно из самых важных действий и за него всегда должен отвечать компетентный специалист. Вы будете смеяться, но грубые оплошности админов происходят слишком часто, чтобы не обращать на них внимание (из моей практики: неоднократно время работы приложения уменьшалось с нескольких часов до нескольких минут (!) из-за очевидной команды UPDATE STATISTICS; фактически, перед анализом плана испонения тяжелых SQL-запросов всегда полезно невзначай поинтересоваться актуальностью статистики. Не менее частым происшествием является "случайная потеря" индекса важной таблицы в результате реорганизации или резервного копирования БД).

Коль скоро среда исполнения правильно сконфигурирована, стоит обратить внимание непосредственно на код. Очевидно, что максимальная скорость эскадры определяется скоростью самого медленного корабля. Он-то нам и нужен. Если "эскадрой" является набор SQL-запросов работающего с БД приложения, то, как правило, никаких трудностей с определением узких мест не возникает. Трудности возникают с определением узких мест "обычных" приложений.

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

Когда критический участок кода локализован, можно приступать к непосредственному анализу. С чего начать? Начинать нужно с самых ресурсоемких операций. Как правило, по требуемому для исполнения времени, операции легко разделяются на слои, отличающиеся друг от друга на несколько порядков:

  1. работа с внешними устройствами
  2. системные вызовы
  3. вызовы собственных функций
  4. локальные управляющие структуры
  5. специальный подбор команд и оптимальное использование регистров
Например, не стоит заниматься вопросами размещения управляющей переменной цикла в соответствующем регистре процессора, если в данном цикле происходит обращение к диску. Вызовы собственных функций существенно отличаются от системных вызовов тем, что когда мы обращаемся к системе, происходит переключение контекста потока (системный код имеет больше привилегий, обращаться к нему можно только через специальные шлюзы) и обязательная проверка достоверности переданных аргументов (например, система проверяет действительно ли ей передана корректная строка путем ее посимвольного сканирования; если при этом произойдет нарушение прав доступа или ошибка адресации, то приложение будет об этом проинформировано; тем самым исключается возможность сбоя внутри ядра системы, когда неясно что делать и кто виноват; наиболее вероятный результат -- blue death screen, system trap и т.д., т.е. невосстановимый сбой самой системы).

Как правило, только в исключительных случаях заметного ускорения работы можно достичь путем локальных улучшений (которыми пестрят древние наставления: a+a вместо 2*a, register int i; и т.д.), современные компиляторы прекрасно справляются с ними без нас (вместе с тем, генерация компилятором недостаточно оптимального кода "в данном конкретном месте" все еще не является редкостью). Серьезные улучшения обычно приносит только изменение алгоритма работы.

Первым делом стоит обратить внимание на сам алгоритм (классическим примером является сортировка с алгоритмами O(N*N), O(N*log(N)) и O(N*M) стоимости или выбор подходящего контейнера). Но не попадите в ловушку! Память современных компьютеров уже не является устройством произвольного доступа, в том смысле, что промах мимо кэша при невинном обращении по указателю может обойтись гораздо дороже вызова тривиальной функции, чей код уже попал в кэш. Известны случаи, когда изменение прохода большой двумерной матрицы с последовательного построчного на "обманывающий" кэш постолбцовый замедляло работу алгоритма в несколько раз!

Если же принципиальный алгоритм изначально оптимален, можно попробовать использовать замену уровней ресурсоемкости. Классическим примером является все то же кэширование. Например вместо дорогостоящего считывания данных с диска, происходит обращение к заранее подготовленной копии в памяти, тем самым мы переходим с первого уровня на второй-третий. Стоит отметить, что техника кэширования находит свое применение не только в работе с внешними устройствами. Если, например, в игровой программе узким местом становится вычисление sin(x), то стоит подумать об использовании заранее рассчитанной таблицы синусов (обычно достаточно 360 значений типа int вместо потенциально более дорогой плаваючей арифметики). Более "прикладной" пример -- это длинный switch по типам сообщений в их обработчике. Если он стал узким местом, подумайте об использовании таблицы переходов или хэширования (стоимость O(1)) или же специальной древовидной структуры (стоимость O(log(N))) -- существенно лучше O(N), обычно обеспечиваемого switch. Ну а про возможность использования виртуальной функции вместо switch я даже не стану напоминать.

Все эти замечания применимы в равной степени к любому языку. Давайте посмотрим на что стоит обратить внимание программистам на C++.

Прежде всего, стоит отметить, что все более-менее существенные маленькие хитрости собственно C++ уже были рассмотрены в предыдущих примерах, так же как и скрытые накладные расходы. Быть может, за кадром осталась только возможность "облегченного вызова функции", т.к. она является не частью (стандартного) C++, а особенностью конкретных реализаций.

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

void f1(int arg)
{
 Var+=arg;
}

void _fastcall f2(int arg)
{
 Var+=arg;
}
функция f1() работала на 50% медленнее. Конечно, реальную выгоду из этого факта можно получить только при массовом использовании функций облегченного вызова во всем проекте. И эта совершенно бесплатная разница может быть достаточно существенной.

Еще один немаловажный фактор -- размер программ. Откуда взялись все эти современные мегабайты? Увы, большая их часть -- мертвый код, реально, более 90% загруженного кода никогда не будет вызвано! Не беда, если эти мегабайты просто лежат на диске, реальные трудности появляются, когда вы загружаете на выполнение несколько таких монстров. Падение производительности системы во время выделения дополнительной виртуальной памяти может стать просто катастрофическим.

Если при разработке большого проекта изначально не придерживаться политики строгого определения зависимостей между исходными файлами (и не принимать серьезных мер для их минимизации), то в итоге, для успешной линковки будет необходимо подключить слишком много мусора из стандартного инструментария данного проекта. В несколько раз больше, чем полезного кода. Из-за чего это происходит? Если функция f() из file1.cpp вызывает g() из file2.cpp, то, очевидно, мы обязаны подключить file2.cpp к нашему проекту. При этом, если не было принято специальных мер, то в file2.cpp почти всегда найдется какая-нибудь g2(), как правило не нужная для работы g() и вызывающая функции еще какого-либо файла; и пошло-поехало... А когда каждое приложение содержит свыше полусотни исходных файлов, а в проекте несколько сотен приложений, то навести порядок постфактум уже не представляется возможным.

Отличное обсуждение локальных приемов оптимизации можно найти у Paul Hsieh "Programming Optimization". Не очень глубокий, а местами и откровенно "слабый", но, тем не менее, практически полезный обзор более высокоуровневых техник представлен в книге Steve Heller "Optimizing C++".


Макросы

В C++ макросы не нужны! До боли знакомое высказывание, не так ли? Я бы его немного уточнил: не нужны, если вы не хотите существенно облегчить себе жизнь.

Я полностью согласен с тем, что чрезмерное и необдуманное использование макросов может вызвать большие неприятности, особенно при повторном использовании кода. Вместе с тем, я не знаю ни одного средства C++, которое могло бы принести пользу при чрезмерном и необдуманном его использовании.

Итак, когда макросы могут принести пользу?

  1. Макрос как надъязыковое средство. Хороший примером является простой, но удивительно полезный отладочный макрос _VAL_, выводящий имя и значение переменной:
    #define _VAL_(var) #var "=" << var << " "
    Надъязыковой частью здесь является работа с переменной как с текстом, путем перевода имени переменной (оно существует только в исходном коде программы) в строковый литерал, реально существующий в коде бинарном. Данную возможность могут предоставить только макросы.
  2. Информация о текущем исходном файле и строке -- ее пользу при отладке трудно переоценить. Для этого я использую специальный макрос _ADD_. Например:
    	cout<<_ADD_("Ошибка чтения");
    выведет что-то вроде
    Ошибка чтения <file.cpp:34>
    А если нужен перевод строки, то стоит попробовать
    	cout<<"Ошибка чтения" _ADD_("") "\n";
    Такой метод работает, потому что макрос _ADD_ возвращает строковый литерал. Вроде бы эквивалентная функция
    	char* _ADD_(char*);
    вполне подошла бы для первого примера, но не для второго. Конечно, для вывода в cout это не имеет никакого значения, но в следующем пункте я покажу принципиальную важность подобного поведения.

    Рассмотрим устройство _ADD_:

    #define _ADD_tmp_tmp_(str,arg) str " <" __FILE__ ":" #arg ">"
    #define _ADD_tmp_(str,arg) _ADD_tmp_tmp_(str,arg)
    #define _ADD_(str) _ADD_tmp_(str,__LINE__)
    Почему все так сложно? Дело в том, что __LINE__ в отличие от __FILE__ является числовым, а не строковым литералом и чтобы привести его к нужному типу придется проявить некоторую смекалку. Мы, конечно, не можем написать:
    #define _ADD_(str) str " <" __FILE__ ":" #__LINE__ ">"
    т.к. # может быть применен только к аргументу макроса. Решением является передача __LINE__ в виде параметра некоторому вспомогательному макросу, но очевидное
    #define _ADD_tmp_(str,arg) str " <" __FILE__ ":" #arg ">"
    #define _ADD_(str) _ADD_tmp_(str,__LINE__)
    не работает: результатом _ADD_("Ошибка чтения") будет
    "Ошибка чтения <file.cpp:__LINE__>"
    что нетрудно было предвидеть. В итоге мы приходим к приведенному выше варианту, который обрабатывается препроцессором следующим образом: _ADD_("Ошибка чтения") последовательно подставляется в
    _ADD_tmp_("Ошибка чтения",__LINE__)
    _ADD_tmp_tmp_("Ошибка чтения",34)
    "Ошибка чтения" " <" "file.cpp" ":" "34" ">"
    "Ошибка чтения <file.cpp:34>"
  3. Получение значения числового макроса в виде строки. Как показывает практика, данная возможность находит себе применение и за пределами подробностей реализации "многоэтажных" макросов. Допустим, что для взаимодействия с SQL-сервером у нас определен класс DB::Query с соответствующей функцией
    void DB::Query::Statement(const char *);
    и мы хотим выбрать все строки некоторой таблицы, имеющие равное некому "магическому числу" поле somefield:
    #define FieldOK 7
    // ...
    DB::Int tmp(FieldOK);
    q.Statement(" SELECT * "
                " FROM sometable "
                " WHERE somefield=? "
    );
    q.SetParam(), tmp;
    Излишне многословно. Как бы это нам использовать FieldOK напрямую? Недостаточно знакомые с возможностями макросов программисты делают это так:
    #define FieldOK 7
    // ...
    #define FieldOK_CHAR "7"
    // ...
    q.Statement(" SELECT * "
                " FROM sometable "
                " WHERE somefield=" FieldOK_CHAR
    );
    В результате чего вы получаете все прелести синхронизации изменений взаимосвязанных наборов макросов со всеми вытекающими из этого ошибками. Правильным решением будет
    #define FieldOK 7
    // ...
    q.Statement(" SELECT * "
                " FROM sometable "
                " WHERE somefield=" _GETSTR_(FieldOK)
    );
    где _GETSTR_ определен следующим образом:
    #define _GETSTR_(arg) #arg
    Кстати, приведенный пример наглядно демонстрирует невозможность полностью эквивалентной замены всех числовых макросов на принятые в C++
    const int FieldOK=7;
    enum { FieldOK=7 };
    макрос _GETSTR_ не сможет с ними работать.
  4. Многократно встречающиеся части кода. Рассмотрим еще один пример из области работы с SQL-сервером. Предположим, что нам нужно выбрать данные из некоторой таблицы. Это можно сделать в лоб:
    struct Table1 {  // представление данных таблицы
           DB::Date  Field1;
           DB::Int   Field2;
           DB::Short Field3;
    };
    
    void f()
    {
     Table1 tbl;
     DB::Query q;
     q.Statement(" SELECT Field1, Field2, Field3 "
                 " FROM Table1 "
     );
     q.BindCol(), tbl.Field1, tbl.Field2, tbl.Field3;
     // ...
    }
    И этот метод действительно работает. Но что, если представление таблицы изменилось? Теперь нам придется искать и исправлять все подобные места -- чрезвычайно утомительный процесс! Об этом стоило позаботиться заранее:
    #define TABLE1_FLD      Field1, Field2, Field3
    #define TABLE1_FLD_CHAR "Field1, Field2, Field3"
    
    struct Table1 {  // представление данных таблицы
           DB::Date  Field1;
           DB::Int   Field2;
           DB::Short Field3;
    
           // вспомогательная функция
           void BindCol(DB::Query& q) { q.BindCol(), TABLE1_FLD; }
    };
    
    void f()
    {
     Table1 tbl;
     DB::Query q;
     q.Statement(" SELECT " TABLE1_FLD_CHAR
                 " FROM Table1 "
     );
     tbl.BindCol(q);
     // ...
    }
    Теперь изменение структуры таблицы обойдется без зубовного скрежета. Стоит отметить, что в определении TABLE1_FLD_CHAR я не мог использовать очевидное _GETSTR_(TABLE1_FLD), т.к. TABLE1_FLD содержит запятые. К сожалению, данное печальное ограничение в примитивном препроцессоре C++ никак нельзя обойти.
  5. Многократно встречающиеся подобные части кода. Представим себе, что мы пишем приложение для банковской сферы и должны выбрать информацию по некоторым счетам. В России, например, счет состоит из многих полей, которые для удобства работы собирают в специальную структуру, а в таблице он может быть представлен смежными полями с одинаковым префиксом:
    q.Statement(" SELECT Field1, AccA_bal, AccA_cur, AccA_key, AccA_brn, "
                " AccA_per, Field2 "
                " FROM Table1 "
    );
    q.BindCol(), tbl.Field1, tbl.AccA.bal, tbl.AccA.cur, tbl.AccA.key,
                 tbl.AccA.brn, tbl.AccA.per, tbl.Field2;
    // ...
    Можете себе представить, сколько писанины требуется для выбора четырех счетов (tbl.AccA, tbl.AccB, tbl.KorA, tbl.KorB). И снова на помощь приходят макросы:
    #define _SACC_(arg) #arg"_bal, "#arg"_cur, "#arg"_key, "#arg"_brn, " \
                        #arg"_per "
    #define _BACC_(arg) arg.bal, arg.cur, arg.key, arg.brn, arg.per
    
    // ...
    
    q.Statement(" SELECT Field1, " _SACC_(AccA) " , Field2 "
                " FROM Table1 "
    );
    q.BindCol(), tbl.Field1, _BACC_(tbl.AccA), tbl.Field2;
    // ...
    Думаю, что комментарии излишни.
  6. Рассмотрим более тонкий пример подобия. Пусть нам потребовалось создать таблицу для хранения часто используемой нами структуры данных:
    struct A {
           MyDate Date;
           int    Field2;
           short  Field3;
    };
    Мы не можем использовать идентификатор Date для имени столбца таблицы, т.к. DATE является зарезервированным словом SQL. Эта проблема легко обходится с помощью приписывания некоторого префикса:
    struct TableA {
           DB::Date  xDate;
           DB::Int   xField2;
           DB::Short xField3;
    
           TableA& operator=(A&);
           void Clear();
    };
    А теперь определим функции-члены:
    TableA& TableA::operator=(A& a)
    {
     xDate=ToDB(a.Date);
     xField2=ToDB(a.Field2);
     xField3=ToDB(a.Field3);
    
     return *this;
    }
    
    void TableA::Clear()
    {
     xDate="";
     xField2="";
     xField3="";
    }
    Гарантирую, что если TableA содержит хотя бы пару-тройку десятков полей, то написание подобного кода вам очень быстро наскучит, мягко говоря! Нельзя ли это сделать один раз, а потом использовать результаты? Оказывается можно:
    TableA& TableA::operator=(A& a)
    {
    // используем склейку лексем: ##
    #define ASS(arg) x##arg=ToDB(a.arg);
     ASS(Date);
     ASS(Field2);
     ASS(Field3);
    #undef ASS
    
     return *this;
    }
    
    void TableA::Clear()
    {
    #define CLR(arg) x##arg=""
     CLR(Date);
     CLR(Field2);
     CLR(Field3);
    #undef CLR
    }
    Теперь определение TableA::Clear()по TableA::operator=() не несет никакой нудной работы, если, конечно, ваш текстовый редактор поддерживает команды поиска и замены. Так же просто можно определить и обратное присваивание: A& A::operator=(TableA&).
Надеюсь, что после приведенных выше примеров вы по-новому посмотрите на роль макросов в C++.
Copyright © С. Деревяго, 2000-2004

Никакая часть данного материала не может быть использована в коммерческих целях без письменного разрешения автора.