На главную | Поиск
Вы находитесь в Хранилище файлов Белорусской цифровой библиотеки

Сергей Деревяго. C++ 3rd: комментарии


© Copyright Сергей Деревяго, 2000 Origin: #cpp3.virtualave.net/cpp3comm/toc

toc

C++ 3rd: комментарии

Сергей Деревяго

C++ 3rd: комментарии


Введение
43 1.3.1. Эффективность и структура
73 2.5.5. Виртуальные функции
79 2.7.2. Обобщенные алгоритмы
128 5.1.1. Ноль
192 7.4. Перегруженные имена функций
199 7.6. Неуказанное количество аргументов
202 7.7. Указатель на функцию
296 10.4.6.2. Члены-константы
297 10.4.7. Массивы
316 11.3.1. Операторы-члены и не-члены
328 11.5.1. Поиск друзей
333 11.7.1. Явные конструкторы
337 11.9. Вызов функции
344 11.12. Класс String
351 12.2. Производные классы
361 12.2. Производные классы
382 13.2.3. Параметры шаблонов
399 13.6.2. Параметризация и наследование
419 14.4.1. Использование конструкторов и деструкторов
421 14.4.2. auto_ptr
422 14.4.4. Исключения и оператор new
431 14.6.1. Проверка спецификаций исключений
431 14.6.3. Отображение исключений
461 15.3.2.1. Множественное наследование и управление доступом
478 15.6. Свободная память
480 15.3.2.1. Множественное наследование и управление доступом
498 16.2.3. STL-контейнеры
505 16.3.4. Конструкторы
508 16.3.5. Операции со стеком
541 17.4.1.2. Итераторы и пары
543 17.4.1.5. Сравнения
555 17.5.3.3. Другие операции
556 17.6. Определение нового контейнера
583 18.4.4.1. Связыватели
584 18.4.4.2. Адаптеры функций-членов
592 18.6. Алгоритмы, модифицирующие последовательность
592 18.6.1. Копирование
622 19.2.5. Обратные итераторы
637 19.4.2. Распределители памяти, определяемые пользователем
641 19.4.4. Неинициализированная память
647 20.2.1. Особенности символов
652 20.3.4. Конструкторы
655 20.3.6. Присваивание
676 21.2.2. Вывод встроенных типов
687 21.3.4. Ввод символов
701 21.4.6.3. Манипуляторы, определяемые пользователем
711 21.6.2. Потоки ввода и буфера
773 23.4.3.1. Этап 1: выявление классов
879 А.5. Выражения
931 B.13.2. Друзья
Оптимизация
Макросы
Исходный код

Copyright © С. Деревяго, 2000

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

intro

Введение

Введение

Вашему вниманию предлагается "еще одна" книга по С++. Что в ней есть? В ней есть все, что нужно для глубокого понимания С++. Дело в том, что практически весь материал основан на блестящей книге Б.Страуструпа "Язык программирования С++", 3е издание. Я абсолютно уверен, что интересующийся С++ программист обязан прочитать "Язык программирования С++", а после прочтения он вряд ли захочет перечитывать описание С++ у других авторов -- маловероятно, что кто-то напишет собственно о С++ лучше д-ра Страуструпа. Моя книга содержит исправления, комментарии и дополнения, но нигде нет повторения уже изложенного материала.

В процессе чтения (и многократного) перечитывания С++ 3rd у меня возникало множество вопросов, большая часть которых отпадала после изучения собственно стандарта и продолжительных раздумий, а за некоторыми приходилось обращаться непосредственно к автору. Хочется выразить безусловную благодарность д-ру Страуструпу за его ответы на все мои, заслуживающие внимания, вопросы и разрешение привести данные ответы здесь.

Как читать эту книгу. Прежде всего, нужно прочитать "Язык программирования С++" и только на этапе второго или третьего перечитывания обращаться к моему материалу, т.к. здесь кроме исправления ошибок русского перевода излагаются и весьма нетривиальные вещи, которые вряд ли будут интересны среднему программисту на С++. Моей целью было улучшить перевод С++ 3rd, насколько это возможно и пролить свет на множество интересных особенностей С++. Кроме того, оригинальное (английское) издание пережило довольно много тиражей, и каждый тираж содержал некоторые исправления, я постарался привести все существенные исправления здесь.

В процессе чтения было бы неплохо иметь под рукой стандарт С++ (ISO/IEC 14882 Programming languages -- C++, First edition, 1998-09-01), или его Final Draft. Также не помешает ознакомиться с классической STL, ведущей начало непосредственно от Алекса Степанова. И самое главное -- не забудьте заглянуть к самому Бьерну Страуструпу.

С уважением, Сергей Деревяго.


Назад Оглавление Вперед

043

1.3.1. Эффективность и структура, стр. 43

1.3.1. Эффективность и структура, стр. 43

За исключением операторов new, delete, type_id, dynamic_cast, throw и блока try, отдельные выражения и инструкции С++ не требуют поддержки во время выполнения.

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

Для демонстрации данной печальной особенности рассмотрим следующую программу:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

long Var, Count;

struct A {
       A();  // не inline
       ~A();
};

void ACon();
void ADes();

void f1()
{
 A a;
}

void f2()
{
 ACon();
 ADes();
}

int main(int argc,char** argv)
{
 if (argc>1) Count=atol(argv[1]);

 clock_t c1,c2;
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000000; j++)
          f1();

  c2=clock();
  printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000000; j++)
          f2();

  c2=clock();
  printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
}

A::A()  { Var++; }
A::~A() { Var++; }

void ACon() { Var++; }
void ADes() { Var++; }

В ней функции f1() и f2() делают одно и то же, только первая неявно, с помощью конструктора и деструктора класса A, а вторая с помощью явного вызова ACon() и ADes().

Для работы программа требует одного параметра -- сколько миллионов раз вызывать тестовые функции. Выберите значение, позволяющее f1() работать несколько секунд и посмотрите на результат для f2(). В зависимости от компилятора и платформы разница может достигать 10 раз!

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

for (long i=0; i<Count; i++)
    for (long j=0; j<1000000; j++)
        f2();

может превратиться в

Var+=Count*2*1000000;

И это просто замечательно, но, к сожалению, не в нашем конкретном случае. Кстати, проверьте качество вашего оптимизатора, перенеся определения A-функций до их вызова в f1() и f2().

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

Итак, накладные расходы присутствуют. А что же inline? Давайте внесем очевидные изменения:

struct A {
       A() { Var++; }
       ~A() { Var++; }
};

void f1()
{
 A a;
}

void f2()
{
 Var++;
 Var++;
}

После данных изменений разницы во времени работы f1() и f2() не должно быть. Но, к несчастью, она будет присутствовать для большинства компиляторов.

Что же происходит? Наблюдаемый нами эффект называется abstraction penalty, т.е. обратная сторона абстракции или налагаемое на нас некачественными компиляторами наказание за использование объектно-ориентированных абстракций.

Посмотрим, как abstraction penalty проявляется в нашем случае. Что же из себя представляет

void f1()
{
 A a;
}

эквивалентное

void f1()  // псевдокод
{
 A::A();
 A::~A();
}

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

void f2()
{
 ACon();
 ADes();
}

В данном случае -- ничем! Но, давайте рассмотрим похожий пример:

void f1()
{
 A a;
 fun();
}

void f2()
{
 ACon();
 fun();
 ADes();
}

Как вы думаете, эквивалентны ли данные функции? Правильный ответ -- нет, т.к. f1() представляет собой

void f1()  // псевдокод
{
 A::A();

 try {
     fun();
 }
 catch (...) {
       A::~A();
       throw;
 }

 A::~A();
}

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

void f1()  // псевдокод
{
 A::A();

 try {
     // пусто
 }
 catch (...) {
       A::~A();
       throw;
 }

 A::~A();
}

Дело в том, что компилятор обязан корректно обрабатывать все возможные случаи, поэтому для упрощения компилятора его разработчики часто не принимают во внимание "частные случаи", в которых можно не генерировать ненужный код. Увы, подобного рода упрощения компилятора очень плохо сказываются на производительности интенсивно использующего средства абстракции и inline функции кода. Хорошим примером подобного рода кода является STL, чье использование, при наличие плохого оптимизатора, вызывает чрезмерные накладные расходы.

Итак, занимаясь оптимизацией "узких мест" обратите внимание на abstraction penalty.


Назад Оглавление Вперед

073

2.5.5. Виртуальные функции, стр. 73

2.5.5. Виртуальные функции, стр. 73

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

На самом деле первое утверждение неверно, т.е. объект полученный в результате множественного наследования от полиморфных классов будет содержать несколько "унаследованных" указателей на vtbl.

Рассмотрим следующий пример. Пусть у нас есть полиморфный (т.е. содержащий виртуальные функции) класс B1:

struct B1 {  // я написал struct чтобы не возиться с правами доступа
       int a1;
       int b1;

       virtual ~B1() { }
};

Пусть имеющаяся у нас реализация размещает vptr (указатель на таблицу виртуальных функций класса) перед объявленными нами членами, тогда данные объекта класса B1 будут расположены в памяти следующим образом:

vptr_1  // указатель на vtbl класса B1
a1      // объявленные нами члены
b1

Если теперь объявить аналогичный класс B2 и производный класс D

struct D: B1, B2 {
       virtual ~D() { }
};

то его данные будут расположены следующим образом:

vptr_d1  // указатель на vtbl класса D, для B1 здесь был vptr_1
a1       // унаследованные от B1 члены
b1
vptr_d2  // указатель на vtbl класса D, для B2 здесь был vptr_2
a2       // унаследованные от B2 члены
b2

Почему здесь два vptr? Потому, что была проведена оптимизация, иначе их было бы три. Я, конечно, понял, что вы имели ввиду: "Почему не один"? Не один, потому что мы имеем возможность преобразовывать указатель на производный класс в указатель на любой из базовых. И полученный указатель должен указывать на корректный объект базового класса. Т.е. если я напишу:

D d;
B2* ptr=&d;

то в нашем примере ptr укажет в точности на vptr_d2. А собственным vptr класса D будет являться vptr_d1. Значения этих указателей, вообще говоря, различны. Почему? Потому что у B1 и B2 в vtbl по одному и тому же индексу могут быть расположены разные функции, а D должен иметь возможность их правильно заместить. Т.о. vtbl класса D состоит из нескольких частей: часть для B1, часть для B2 часть для собственных нужд.

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

И раз уж так много слов было сказано про эффективность, давайте реально измерим относительную стоимость вызова виртуальной функции.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

long Var, Count;

struct B {
       void f();
       virtual void vf();
};

struct D : B {
       void vf();  // замещаем B::vf
};

void f1(B* ptr)
{
 ptr->f();
}

void f2(B* ptr)
{
 ptr->vf();
}

int main(int argc,char** argv)
{
 if (argc>1) Count=atol(argv[1]);

 clock_t c1,c2;

 D d;
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000000; j++)
          f1(&d);

  c2=clock();
  printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000000; j++)
          f2(&d);

  c2=clock();
  printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
}

void B::f()  { Var++; }
void B::vf() { }

void D::vf() { Var++; }

В зависимости от компилятора, накладные расходы на вызов виртуальной функции составили от 5 до 20 процентов. Т.о. вызов виртуальной функции не несет существенных накладных расходов -- очень хорошая новость.


Назад Оглавление Вперед

079

2.7.2. Обобщенные алгоритмы, стр. 79

2.7.2. Обобщенные алгоритмы, стр. 79

Встроенные в С++ типы низкого уровня, такие как указатели и массивы, имеют соответствующие операции, поэтому мы можем записать:

char vc1[200];
char vc2[500];

void f()
{
 copy(&vc1[0],&vc1[200],&vc2[0]);
}

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

The issue is whether taking the address of one-past-the-last element of an array is conforming C and C++. I could make the example clearly conforming by a simple rewrite:

 copy(vc1,vc1+200,vc2);

However, I don't want to introduce addition to pointers at this point of the book. It is a surprise to most experienced C and C++ programmers that &vc1[200] isn't completely equivalent to vc1+200. In fact, it was a surprise to the C committee also and I expect it to be fixed in the upcoming revision of the standard. (also resolved for C9x - bs 10/13/98).

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

 copy(vc1,vc1+200,vc2);

Однако, я не хотел вводить сложение с указателем в этой части книги. Даже для самых опытных программистов на С и С++ большим сюрпризом является тот факт, что &vc1[200] не полностью эквивалентно vc1+200. Фактически, это оказалось неожиданностью и для С комитета, и я надеюсь, что это недоразумение будет устранено в следующих редакциях стандарта.

Так в чем же нарушается эквивалентность? По стандарту С++ мы имеем следующие эквивалентные преобразования:

&vc1[200] == &(*((vc1)+(200))) == &*(vc1+200)

Действительно ли равенство &*(vc1+200) == vc1+200 неверно?

It is false in C89 and C++, but not in K&R C or C9x. The C89 standard simply said that &*(vc1+200) means dereference vc1+200 (which is an error) and then take the address of the result, and the C++ standard copiled the C89 wording. K&R C and C9x say that &* cancels out so that &*(vc1+200) == vc2+200.

Это неверно в С89 и С++, но не в K&R С или С9х. Стандарт С89 говорит, что &*(vc1+200) означает разыменование vc1+200 (что является ошибкой) и затем взятие адреса результата. И стандарт С++ просто взял эту формулировку из С. Однако K&R С и С9х устанавливают, что &* взаимно уничтожаются, т.е. &*(vc1+200) == vc1+200.

Спешу вас успокоить, что на практике в выражении &*(vc1+200) некорректное разыменование *(vc1+200) практически никогда не произойдет, т.к. результатом всего выражения является адрес и ни один серьезный компилятор не станет выбирать значение по некоторому адресу (операция разыменования) чтобы потом получить тот же самый адрес с помощью операции &.


Назад Оглавление Вперед

128

5.1.1. Ноль, стр. 128

5.1.1. Ноль, стр. 128

Если вы чувствуете, что просто обязаны определить NULL, воспользуйтесь

const int NULL=0;

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

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>

void error(int stat ...)
{
 va_list ap;
 va_start(ap,stat);

 while (const char* sarg=va_arg(ap,const char *))
       printf("%s",sarg);

 va_end(ap);
 exit(stat);
}

int main()
{
 error(1,"Случилось ","страшное",NULL);
}

Здесь описана функция error() с переменным числом параметров. Она печатает переданные ей строки, пока не встретит нулевой указатель и прекращает выполнение программы с заданным кодом завершения. Если в нашей реализации sizeof(char*)>sizeof(int), и NULL определен через int, то при запуске приведенного кода нас ждут большие неприятности.

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

error(1,"Случилось ","страшное",static_cast<const char *>(0));

Каюсь, ненужная писанина выводит меня из себя, и в реальной программе я бы использовал эквивалентное:

error(1,"Случилось ","страшное",(char *)0);

хоть это и не педагогично.


Назад Оглавление Вперед

192

7.4. Перегруженные имена функций, стр. 192

7.4. Перегруженные имена функций, стр. 192

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

Приведенный в книге пункт 2 нужно заменить на:

[2] Соответствие, достигаемое "продвижением" ("повышением в чине") интегральных типов (например, bool в int, char в int, short в int; § B.6.1), float в double.

Также следует отметить, что доступность функций-членов не влияет на процесс поиска подходящей функции, например:

struct A {
 private:
       void f(int);
 public:
       void f(...);
};

void g()
{
 A a;
 a.f(1);  // ошибка: выбирается A::f(int), использование
          // которой в g() запрещено
}

Отсутствие данного правила породило бы тонкие ошибки, когда выбор подходящей функции зависел бы от места вызова: в функции-члене или в обычной функции.


Назад Оглавление Вперед

199

7.6. Неуказанное количество аргументов, стр. 199

7.6. Неуказанное количество аргументов, стр. 199

До выхода из функции, где была использована va_start(), необходимо осуществить вызов va_end(). Причина состоит в том, что va_start() может модифицировать стек таким образом, что станет невозможен нормальный выход из функции.

Ввиду чего возникают совершенно незаметные подводные камни. Общеизвестно, что обработка исключения предполагает раскрутку стека. Следовательно, если в момент возбуждения исключения функция изменила стек, то у вас гарантированно будут неприятности. Таким образом, до вызова va_end() следует воздерживаться от потенциально вызывающих исключения операций. Специально добавлю, что ввод/вывод С++ может генерировать исключения, т.е. "наивная" техника вывода в std::cout до вызова va_end() чревата неприятностями.

Д-р Страуструп пишет по этому поводу следующее:

It is your job to ensure that va_end() is called. The language and standard library offers no guarantees. I don't recommend the use of varargs.

Это ваше дело следить за тем, чтобы va_end() была вызвана. Язык и стандартная библиотека не дают никаких гарантий. Я бы не рекомендовал использовать технику stdarg.


Назад Оглавление Вперед

202

7.7. Указатель на функцию, стр. 202

7.7. Указатель на функцию, стр. 202

Причина в том, что разрешение использования cmp3 в качестве аргумента ssort() нарушило бы гарантию того, что ssort() вызовется с аргументами mytype*.

Здесь имеет место досадная опечатка, совершенно искажающая смысл предложения. Следует читать так: Причина в том, что разрешение использования cmp3 в качестве аргумента ssort() нарушило бы гарантию того, что cmp3() вызовется с аргументами mytype*.


Назад Оглавление Вперед

296

10.4.6.2. Члены-константы, стр. 296

10.4.6.2. Члены-константы, стр. 296

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

Вроде бы все хорошо, но почему только интегрального типа? В чем причина подобной дискриминации? Д-р Страуструп пишет по этому поводу следующее:

The reason for "discriminating against" floating points in constant expressions is that the precision of floating point traditionally varied radically between processors. In principle, constant expressions should be evaluated on the target processor if you are cross compiling.

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

От себя добавлю, что звучит это немного нелогично, т.к. я могу написать так:

class Curious {
      static const float c5;
};

const float Curious::c5=7.5f;

а вот такой код:

class Curious { static const float c5=7.5f; };

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


Назад Оглавление Вперед

297

10.4.7. Массивы, стр. 297

10.4.7. Массивы, стр. 297

Не существует способа явного указания аргументов конструктора (за исключением использования списка инициализации) при объявлении массива.

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

#include <stdio.h>

struct A {  // исходный класс
       int a;
       A(int a_) : a(a_) { printf("%d\n",a); }
};

void fun()
{
 static int s;

 struct A_fun : public A {  // вспомогательный локальный
        A_fun() : A(s++) { }
 };

 A_fun arr[5];
 // и далее используем как A arr[5];
}

int main()
{
 fun();
}

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

9.8. Объявления локальных классов (Local class declarations [class.local])

- 1 - Класс может быть определен внутри функции; такой класс называется локальным (local) классом. Имя локального класса является локальным в окружающем контексте (enclosing scope). Локальный класс находится в окружающем контексте и имеет тот же доступ к именам вне функции, что и у самой функции. Объявления в локальном классе могут использовать только имена типов, статические переменные, extern переменные и функции, перечисления из окружающего контекста. Например:

int x;
void f()
{
 static int s;
 int x;
 extern int g();

 struct local {
        int g() { return x; }   // ошибка, auto x
        int h() { return s; }   // OK
        int k() { return ::x; } // OK
        int l() { return g(); } // OK
 };
 //  ...
}

local* p = 0;  // ошибка: нет local в текущем контексте

- 2 - Окружающая функция не имеет специального доступа к членам локального класса, она имеет обычные права доступа (см. class.access). Функции-члены локального класса, если они есть, должны быть определены внутри определения класса.

- 3 - Если класс X -- локальный класс, то в нем может быть объявлен вложенный класс Y, который может быть определен позднее, в определении класса X; или он может быть определен позднее в том же контексте, что и X. Вложенный класс локального класса сам является локальным.

- 4 - Локальный класс не может иметь статических данных-членов.


Назад Оглавление Вперед

316

11.3.1. Операторы-члены и не-члены, стр. 316

11.3.1. Операторы-члены и не-члены, стр. 316

complex r1=x+y+z;  // r1=operator+(x,operator+(y,z))

На самом деле данное выражение будет проинтерпретировано так:

complex r1=x+y+z;  // r1=operator+(operator+(x,y),z)

Потому что операция сложения левоассоциативна: (x+y)+z.


Назад Оглавление Вперед

328

11.5.1. Поиск друзей, стр. 328

11.5.1. Поиск друзей, стр. 328

Приведенный в конце данной страницы пример нужно заменить на:

// нет f() в данной области видимости

class X {
      friend void f();          // бесполезно
      friend void h(const X&);  // может быть найдена по аргументам
};

void g(const X& x)
{
 f();   // нет f() в данной области видимости
 h(x);  // h() -- друг X
}

Он взят из списка авторских исправлений к 8-му тиражу.


Назад Оглавление Вперед

333

11.7.1. Явные конструкторы, стр. 333

11.7.1. Явные конструкторы, стр. 333

Разница между

String s1='a';  // ошибка: нет явного преобразования char в String
String s2(10);  // правильно: строка для хранения 10 символов

может показаться очень тонкой...

На самом деле разница сразу же заметна невооруженным глазом. Если мы пишем

X a=b;

то это подразумевает, что мы хотим создать объект a класса X , проинициализировав его значением объекта b некоторого класса. А инициализация всегда означает копирование, т.е. если b того же типа, что и a, то сразу вызывается конструктор копирования, что эквивалентно записи

X a(b);

Если же объект b имеет другой тип, то с помощью соответствующего конструктора (неявно) создается временный объект класса X и далее все идет по накатанной колее. То есть

X a(X(b));

Надеюсь, что после этих объяснений разница между объявлением s1 и s2 стала очевидной.

Ввиду всего вышесказанного хотелось бы отметить, что для типов, определяемых пользователем, не стоит использовать нотацию

X a=b;

т.к. в лучшем случае она эквивалентна более явному

X a(b);

а в худшем приводит к совершенно излишнему созданию временных объектов, как, например, в объявлении

complex z=5;

Вместе с тем, реализация вправе не создавать ненужные временные объекты. Вот соответствующая часть стандарта.

12.8. Копирование объектов класса (Copying class objects)

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

Давайте не поленимся и напишем маленький класс, позволяющий отследить возникающие при этом спецэффекты.

#include <stdio.h>
#include <string.h>

struct A {
       static const int nsize=10;

       char n[nsize];

       A(char cn) {
         n[0]=cn;
         n[1]=0;
         printf("%5s.A::A()\n",n);
       }

       A(const A& a) {
         if (strlen(a.n)<=nsize-2) {
            n[0]='?';
            strcpy(n+1,a.n);
         }
         else strcpy(n,"беда");
         printf("%5s.A::A(const A& %s)\n",n,a.n);
       }

       ~A() {
          printf("%5s.A::~A()\n",n);
       }

       A& operator=(const A& a) {
            if (strlen(a.n)<=nsize-2) {
               n[0]='=';
               strcpy(n+1,a.n);
            }
            else strcpy(n,"беда");
            printf("%5s.A::operator=(const A& %s)\n",n,a.n);
            return *this;
       }

};

A f1(A a)
{
 printf("A f1(A %s)\n",a.n);
 return a;
}

A f2()
{
 printf("A f2()\n");
 A b('b');
 return b;
}

A f3()
{
 printf("A f3()\n");
 return A('c');
}

int main()
{
 {
  A a('a');
  A b='b';
  A c(A('c'));
  A d=A('d');
 }
 printf("----------\n");
 {
  A a('a');
  A b=f1(a);
  printf("b это %s\n",b.n);
 }
 printf("----------\n");
 {
  A a=f2();
  printf("a это %s\n",a.n);
 }
 printf("----------\n");
 {
  A a=f3();
  printf("a это %s\n",a.n);
 }
}

Прежде всего, в main() разными способами создаются объекты a, b, c и d. В нормальной реализации вы получите следующий вывод:

    a.A::A()
    b.A::A()
    c.A::A()
    d.A::A()
    d.A::~A()
    c.A::~A()
    b.A::~A()
    a.A::~A()

Там же, где разработчики компилятора схалтурили, появятся ненужные временные объекты, например:

    . . .
    c.A::A()
   ?c.A::A(const A& c)
    c.A::~A()
    d.A::A()
    d.A::~A()
   ?c.A::~A()
    . . .

Т.е. A c(A('c')) превратилось в A tmp('c'), c(tmp). Далее, вызов f1() демонстрирует неявные вызовы конструкторов копирования во всей красе:

    a.A::A()
   ?a.A::A(const A& a)
A f1(A ?a)
  ??a.A::A(const A& ?a)
   ?a.A::~A()
b это ??a
  ??a.A::~A()
    a.A::~A()

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

Полностью согласен -- все это действительно очень запутанно, но разобраться все же стоит. Для более явной демонстрации исключения временной переменной я написал f2() и f3():

A f2()
    b.A::A()
   ?b.A::A(const A& b)
    b.A::~A()
a это ?b
   ?b.A::~A()

A f3() c.A::A() a это c c.A::~A()

В f2() оно происходит, а в f3() -- нет, как говорится, все дело в волшебных пузырьках. Другого объяснения нет, т.к. временная переменная должна была исключиться в обоих случаях (ох уж мне эти писатели компиляторов!).

А сейчас рассмотрим более интересный случай -- перегрузку операторов. Внесем в наш класс соответствующие изменения:

#include <stdio.h>
#include <string.h>

struct A {
       static const int nsize=10;
       static int tmpcount;

       int val;
       char n[nsize];

       A(int val_) : val(val_) {  // для создания временных объектов
         sprintf(n,"_%d",++tmpcount);
         printf("%5s.A::A(int %d)\n",n,val);
       }

       A(char cn,int val_) : val(val_) {
         n[0]=cn;
         n[1]=0;
         printf("%5s.A::A(char,int %d)\n",n,val);
       }

       A(const A& a) : val(a.val) {
         if (strlen(a.n)<=nsize-2) {
            n[0]='?';
            strcpy(n+1,a.n);
         }
         else strcpy(n,"беда");
         printf("%5s.A::A(const A& %s)\n",n,a.n);
       }

       ~A() {
          printf("%5s.A::~A()\n",n);
       }

       A& operator=(const A& a) {
            val=a.val;
            if (strlen(a.n)<=nsize-2) {
               n[0]='=';
               strcpy(n+1,a.n);
            }
            else strcpy(n,"беда");
            printf("%5s.A::operator=(const A& %s)\n",n,a.n);
            return *this;
       }

       friend A operator+(const A& a1,const A& a2) {
         printf("operator+(const A& %s,const A& %s)\n",a1.n,a2.n);
         return A(a1.val+a2.val);
       }
};

int A::tmpcount;

int main()
{
 A a('a',1), b('b',2), c('c',3);
 A d=a+b+c;
 printf("d это %s\n",d.n);
 printf("d.val=%d\n",d.val);
}

После запуска вы должны получить следующие результаты:

    a.A::A(char,int 1)
    b.A::A(char,int 2)
    c.A::A(char,int 3)
operator+(const A& a,const A& b)
   _1.A::A(int 3)
operator+(const A& _1,const A& c)
   _2.A::A(int 6)
   _1.A::~A()
d это _2
d.val=6
   _2.A::~A()
    c.A::~A()
    b.A::~A()
    a.A::~A()

Все довольно наглядно, так что объяснения излишни. А для демонстрации работы оператора присваивания попробуйте

 A d('d',0);
 d=a+b+c;

В данном случае будет задействовано на одну временную переменную больше:

    a.A::A(char,int 1)
    b.A::A(char,int 2)
    c.A::A(char,int 3)
    d.A::A(char,int 0)
operator+(const A& a,const A& b)
   _1.A::A(int 3)
operator+(const A& _1,const A& c)
   _2.A::A(int 6)
  =_2.A::operator=(const A& _2)
   _2.A::~A()
   _1.A::~A()
d это =_2
d.val=6
  =_2.A::~A()
    c.A::~A()
    b.A::~A()
    a.A::~A()

Назад Оглавление Вперед

337

11.9. Вызов функции, стр. 337

11.9. Вызов функции, стр. 337

Функция, которая вызывается повторно, -- это operator()() объекта Add(z).

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

Рассмотрим, например, определение функции-шаблона for_each()

template <class InputIter, class Function>
Function for_each(InputIter first, InputIter last, Function f) {
 for ( ; first != last; ++first)
     f(*first);
 return f;
}

Данное определение я взял непосредственно из SGI STL (предварительно убрав символы подчеркивания для улучшения читаемости). Если его сравнить с приведенным в книге, то сразу бросается в глаза исправление типа возвращаемого значения (по стандарту должен быть аргумент-функция) и отказ от использования потенциально менее эффективного постинкремента итератора.

Когда мы вызываем for_each() c аргументом Add(z),

for_each(ll.begin(),ll.end(),Add(z));

то Function -- это Add, т.е. тип, а не объект Add(z). И по определению for_each() компилятором будет сгенерирован следующий код:

Add for_each(InputIter first, InputIter last, Add f) {
 for ( ; first != last; ++first)
     f.operator()(*first);
 return f;
}

Т.о. в момент вызова for_each() будет создан временный объект Add(z), который затем и будет передан в качестве аргумента. После чего, внутри for_each() для копии этого объекта будет вызываться Add::operator()(complex&). Конечно, тип InputIter также будет заменен типом соответствующего итератора, но в данный момент это нас не интересует.

На что же я хочу обратить ваше внимание? Я хочу отметить, что шаблон -- это не макрос в который передается что-то, к чему можно приписать скобки с соответствующими аргументами. Если бы шаблон был макросом, непосредственно принимающим переданный объект, то мы бы получили

Add for_each(. . .) {
 for (. . .)
     Add(z).operator()(*first);
 return f;
}

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


Назад Оглавление Вперед

344

11.12. Класс String, стр. 344

11.12. Класс String, стр. 344

Обратите внимание, что для неконстантного объекта, s.operator[](1) означает Cref(s,1).

А вот здесь хотелось бы по-подробнее. Почему в одном классе мы можем объявить const и не const функции-члены? Как осуществляется выбор перегруженной функции?

Рассмотрим следующее объявление:

struct X {
  void f(int);
  void f(int) const;
};

void h()
{
 const X cx;
 cx.f(1);

 X x;
 x.f(2);
}

Ввиду того, что функция-член всегда имеет скрытый параметр this, компилятор воспринимает данное объявление как

// псевдокод
struct X {
  void f(      X *const this);
  void f(const X *const this);
};

void h()
{
 const X cx;
 X::f(&cx,1);

 X x;
 X::f(&x,2);
}

и выбор перегруженной функции осуществляется по обычным правилам. В общем -- никакой мистики.


Назад Оглавление Вперед

351

12.2. Производные классы, стр. 351

12.2. Производные классы, стр. 351

Базовый класс иногда называют суперклассом, а производный -- подклассом. Однако подобная терминология вводит в заблуждение людей, которые замечают, что данные в объекте производного класса являются надмножеством данных базового класса.

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


Назад Оглавление Вперед

361

12.2. Производные классы, стр. 361

12.2. Производные классы, стр. 361

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

Это, вообще говоря, неверно. При применении множественного наследования "просто косвенного вызова" оказывается недостаточно. Рассмотрим следующую программу:

#include <iostream.h>

struct B1 {
       int b1;  // непустая
};

struct B2 {
       int b2;  // непустая
       virtual void vfun() { }
};

struct D : B1, B2 {  // множественное наследование от непустых классов
       virtual void vfun() {
               cout<<"D::vfun(): this="<<this<<"\n";
       }
};

int main()
{
 D d;

 D* dptr=&d;
 cout<<"dptr\t"<<dptr<<"\n";
 dptr->vfun();

 B2* b2ptr=&d;
 cout<<"b2ptr\t"<<b2ptr<<"\n";
 b2ptr->vfun();
}

На своей машине я получил следующие результаты:

dptr    0x283fee8
D::vfun(): this=0x283fee8
b2ptr   0x283feec
D::vfun(): this=0x283fee8

Т.е. при вызове через указатель на производный класс dptr, внутри D::vfun() мы получим this=0x283fee8. Но несмотря на то, что после преобразования исходного указателя в указатель на (второй) базовый класс b2ptr, его значение (очевидно) изменилось, внутри D::vfun() мы все равно видим исходное значение, что полностью соответствует ожиданиям D::vfun() относительно типа и значения своего this.

Что же все это означает? А означает это то, что если бы вызов виртуальной функции

struct D : B1, B2 {
 virtual void vfun(D *const this) {  //  псевдокод
         // . . .
 }
};

через указатель ptr->vfun() всегда сводился бы к вызову vfun(ptr), то в нашей программе мы бы получили this==b2ptr==0x283fee8.

Вопрос номер два: как они это делают? Элементарно, Ватсон! (Не переборщил?) Известно, что виртуальная функция производного класса замещает (override) соответствующую функцию базового класса, а именно: в соответствующую позицию vtbl записывается адрес функции производного класса. Только вот в часть vtbl, относящуюся лично к классу D будет записан адрес собственно D::vfun(), а в часть, относящуюся к подъобекту B2 будет записан адрес некоторой, сгенерированной компилятором функции vfun_hack():

// псевдокод
void vfun_hack(B2 *const this)  // обратите внимание: указатель на B2
{
 return D::vfun(static_cast<D*>(this));  // да, return void;
}

Хотя наверняка оптимизатор исключит вложенный вызов функции с помощью простого goto D::vfun, однако для простоты восприятия об этом лучше не говорить.


Назад Оглавление Вперед

382

13.2.3. Параметры шаблонов, стр. 382

13.2.3. Параметры шаблонов, стр. 382

В частности, строковый литерал не допустим в качестве аргумента шаблона.

Потому что строковый литерал -- это объект с внутренней компоновкой (internal linkage).


Назад Оглавление Вперед

399

13.6.2. Параметризация и наследование, стр. 399

13.6.2. Параметризация и наследование, стр. 399

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

Прекрасный образчик дословного перевода. Сразу же вспоминается другой, не менее удачный перевод:

-- How do you do? // Как вы это делаете?
-- It's all right. // Все время правой.

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

Далее хочу отметить, что постоянно встречающуюся в переводе фразу "конструктор шаблона" следует понимать как "конструктор-шаблон".


Назад Оглавление Вперед

419

14.4.1. Использование конструкторов и деструкторов, стр. 419

14.4.1. Использование конструкторов и деструкторов, стр. 419

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

Если вы решили, что тем самым должна повыситься производительность, ввиду того, что в теле функции отсутствуют блоки try/catch, то должен вас огорчить -- они будут автоматически сгенерированы компилятором, для корректной обработки раскрутки стека. Но все-таки, какая версия выделения ресурсов обеспечивает большую производительность? Давайте протестируем следующий код:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

long Var, Count;

void ResourceGet();
void ResourceReturn();
void Work();

struct SafeResource {
       SafeResource()  { ResourceGet(); }
       ~SafeResource() { ResourceReturn(); }
};

void f1()
{
 ResourceGet();
 try {
     Work();
 }
 catch (...) {
       ResourceReturn();
       throw;
 }
 ResourceReturn();
}

void f2()
{
 SafeResource sr;
 Work();
}

int main(int argc,char** argv)
{
 if (argc>1) Count=atol(argv[1]);

 clock_t c1,c2;

 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000000; j++)
          f1();

  c2=clock();
  printf("f1(): %ld mln calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000000; j++)
          f2();

  c2=clock();
  printf("f2(): %ld mln calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
}

void ResourceGet()     { Var++; }

void ResourceReturn()  { Var--; }

void Work() { Var+=2; }

Как выдумаете, какая функция работает быстрее? А вот и нет! В зависимости от компилятора быстрее работает то f1(), то f2(), а иногда они работают совершенно одинаково из-за полной идентичности сгенерированного компилятором кода. Все зависит от используемых принципов обработки исключений и качества оптимизатора.

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

void g1(), g2();

void f()
{
 g1();
 g2();
}

void g1()
{
 // 1
 throw E();
 // 2
}

Для метода ЕВ сгенерированный код будет выглядеть следующим образом:

extern bool InException;

void f()
{
 g1();
 if (InException) return;

 g2();
}

void g1()
{
 // 1
 CopyAndSave(E());
 InException=true;
 return;
 // 2
}

Т.е. происходит простое отображение исключений на традиционно применяемую в С/C++ технику проверки кодов возврата. Конечно, для возвращающих значения и создающих объекты функций картина значительно усложняется.

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

Каждый из методов обладает своими достоинствами и недостатками, так, например, для метода РС пролог/эпилог функций становится довольно "тяжелым", каждая функция обязана устанавливать стековый фрейм, метод ЕВ здесь существенно выигрывает в производительности. С другой стороны, если мы должны осуществить возврат из глубоко рекурсивной функции, не создававшей объектов, то для метода РС работа заключается в простом сбросе стека, а ЕВ будет вынужден проделать долгий обратный путь.

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

It is possible to drive the exception handling overhead down below 1% run-time overhead in real implementations - I know because it has been done.

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

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

Резюмируя данный раздел, хочется отметить, что как расширенное использование шаблонов совершенно изменило внешний вид стандартного С++, исключения кардинально изменили вид внутренний. С++ начал свою жизнь в UNIX с транслятора cfront, который просто переводил код С++ в эквивалентный код С, однако после года работы над cfront 4.0, когда он уже был написан, команда разработчиков cfront решила от него отказаться, т.к. удовлетворяющий всем требованиям стандарта механизм обработки исключений оказался совершенно неэффективным при его реализации непосредственно на С.


Назад Оглавление Вперед

421

14.4.2. auto_ptr, стр. 421

14.4.2. auto_ptr, стр. 421

В стандартном заголовочном файле <memory> auto_ptr объявлен следующим образом...

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

Для достижения данной семантики владения (также называемой семантикой разрушения после копирования (destructive copy semantics)), семантика копирования шаблона auto_ptr радикально отличается от семантики копирования обычных указателей: когда один auto_ptr копируется или присваивается другому, исходный auto_ptr очищается (эквивалентно присваиванию 0 указателю). Т.к. копирование auto_ptr приводит к его изменению, то const auto_ptr не может быть скопирован.

Шаблон auto_ptr определен в <memory> следующим образом:

template<class X> class std::auto_ptr {
 // вспомогательный класс
 template <class Y> struct auto_ptr_ref { /* ... */ };

 X* ptr;
public:
 typedef X element_type;

 explicit auto_ptr(X* p =0) throw() { ptr=p; }
 ~auto_ptr() throw() { delete ptr; }

 // обратите внимание: конструкторы копирования и операторы
 // присваивания имеют неконстантные аргументы

 // скопировать, потом a.ptr=0
 auto_ptr(auto_ptr& a) throw();

 // скопировать, потом a.ptr=0
 template<class Y> auto_ptr(auto_ptr<Y>& a) throw();

 // скопировать, потом a.ptr=0
 auto_ptr& operator=(auto_ptr& a) throw();

 // скопировать, потом a.ptr=0
 template<class Y> auto_ptr& operator=(auto_ptr<Y>& a) throw();

 X& operator*() const throw() { return *ptr; }
 X* operator->() const throw() { return ptr; }

 // вернуть указатель
 X* get() const throw() { return ptr; }

 // передать владение
 X* release() throw() { X* t = ptr; ptr=0; return t; }

 void reset(X* p =0) throw() { if (p!=ptr) { delete ptr; ptr=p; } }

 // скопировать из auto_ptr_ref
 auto_ptr(auto_ptr_ref<X>) throw();

 // скопировать в auto_ptr_ref
 template<class Y> operator auto_ptr_ref<Y>() throw();

 // разрушающее копирование из auto_ptr
 template<class Y> operator auto_ptr<Y>() throw();
};

Назначение auto_ptr_ref -- обеспечить семантику уничтожения после копирования, ввиду чего копирование константного auto_ptr становится невозможным. Конструктор-шаблон и оператор присваивания-шаблон обеспечивают возможность неявного пребразования auto_ptr<D> в auto_ptr<B> если D* может быть преобразован в B*, например:

void g(Circle* pc)
{
 auto_ptr<Circle> p2 = pc;  // сейчас p2 отвечает за удаление

 auto_ptr<Circle> p3 = p2;  // сейчас p3 отвечает за удаление,
                            // а p2 уже нет

 p2->m = 7;                 // ошибка программиста: p2.get()==0

 Shape* ps = p3.get();      // извлечение указателя

 auto_ptr<Shape> aps = p3;  // передача прав собственности и
                            // преобразование типа

 auto_ptr<Circle> p4 = pc;  // ошибка: теперь p4 также отвечает за удаление
}

Эффект от использования нескольких auto_ptr для одного и того же объекта неопределен; в большинстве случаев объект будет уничтожен дважды, что приведет к разрушительным результатам.

Следует отметить, что семантика уничтожения после копирования не удовлетворяет требованиям для элементов стандартных контейнеров или стандартных алгоритмов, таких как sort(). Например:

// опасно: использование auto_ptr в контейнере
void h(vector<auto_ptr<Shape> >& v)
{
 sort(v.begin(),v.end());  // не делайте так: элементы не будут отсортированы
}

Понятно, что auto_ptr не является обычным "умным" указателем, однако он прекрасно справляется с предоставленной ему ролью -- обеспечивать безопасную относительно исключений работу с автоматическими указателями, и делать это без существенных накладных расходов.


Назад Оглавление Вперед

422

14.4.4. Исключения и оператор new, стр. 422

14.4.4. Исключения и оператор new, стр. 422

При некотором использовании этого синтаксиса выделенная память затем освобождается, при некотором -- нет.

Т.к. приведенные в книге объяснения немного туманны, вот соответствующая часть стандарта:

5.3.4. New [expr.new]

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


Назад Оглавление Вперед

431

14.6.1. Проверка спецификаций исключений, стр. 431

14.6.1. Проверка спецификаций исключений, стр. 431

Спецификация исключений не является частью типа функции, и typedef не может ее содержать.

Сразу же возникает вопрос: в чем причина этого неудобного ограничения? Д-р Страуструп пишет по этому поводу следующее:

The reason is the exception spacification is not part of the type; it is a constraint that is checked on assignment and exforced at run time (rather than at compile time). Some people would like it to be part of the type, but it isn't. The reason is to avoid difficulties when updating large systems with parts from different sources. See "The Design and Evolution of C++" for details.

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

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


Назад Оглавление Вперед

431a

14.6.3. Отображение исключений, стр. 431

14.6.3. Отображение исключений, стр. 431

В настоящее время стандарт не поддерживает отображение исключений в std::bad_exception описанным в данном разделе образом. Вот что об этом пишет д-р Страуструп:

The standard doesn't support the mapping of exceptions as I describe it in 14.6.3. It specifies mapping to std::bad_exception for exceptions thrown explicitly within an unexpected() function. This makes std::bad_exception an ordinary and rather pointless exception. The current wording does not agree with the intent of the proposer of the mechanism (Dmitry Lenkov of HP) and what he thought was voted in. I have raised the issue in the standards committee.

Стандарт не поддерживает отображение исключений в том виде, как это было описано в разделе 14.6.3. Он специфицирует отображение в std::bad_exception только для исключений, сгенерированных в unexpected(). Это лишает std::bad_exception первоначального смысла, делая его обычным и отчасти бессмысленным исключением. Текущая формулировка (стандарта) не совпадает с первоначально предложенной Дмитрием Ленковым из HP. Комитет стандартов поставлен в известность.

Ну и раз уж столько слов было сказано про формулировку из стандарта, думаю, что стоит здесь ее привести:

15.5.2 Функция unexpected() [except.unexpected]

- 1 - Если функция со спецификацией исключений возбуждает исключение не принадлежащее ее спецификации, будет вызвана функция

 void unexpected();

сразу же после завершения раскрутки стека (stack unwinding).

- 2 - Функция unexpected() не может вернуть управление, но может (пере)возбудить исключение. Если она возбуждает новое исключение, которое разрешено нарушенной до этого спецификацией исключений, то поиск подходящего обработчика будет продолжен с точки вызова сгенерировавшей неожиданное исключение функции. Если же она возбудит недозволенное исключение, то: Если спецификация исключений не содержит класс std::bad_exception (18.6.2.1), то будет вызвана terminate(), иначе (пере)возбужденное исключение будет заменено на определяемый реализацией объект типа std::bad_exception и поиск соответствующего обработчика будет продолжен описанным выше способом.

- 3 - Таким образом, спецификация исключений гарантирует, что могут быть возбуждены только перечисленные исключения. Если спецификация исключений содержит класс std::bad_exception, то любое неописанное исключение может быть заменено на std::bad_exception внутри unexpected().


Назад Оглавление Вперед

461

15.3.2.1. Множественное наследование и управление доступом, стр. 461

15.3.2.1. Множественное наследование и управление доступом, стр. 461

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

Тут, конечно, имеет место досадная опечатка, что, кстати сказать, сразу видно из приведенного примера. Т.е. читать следует так: ... если он разрешен по некоторому из возможных путей.


Назад Оглавление Вперед

478

15.6. Свободная память, стр. 478

15.6. Свободная память, стр. 478

В принципе, освобождение памяти осуществляется тогда внутри деструктора (который знает размер).

Именно так. Т.е. если вы объявили деструктор некоторого класса

A::~A()
{
 // тело деструктора
}

то компилятором (чаще всего) будет сгенерирован следующий код

// псевдокод
A::~A(A *const this,bool flag)
{
 if (this) {
    // тело деструктора
    if (flag) delete(this,sizeof(A));
 }
}

Ввиду чего функция

void f(Employee* ptr)
{
 delete ptr;
}

превратится в

// псевдокод
void f(Employee* ptr)
{
 Employee::~Employee(ptr,true);
}

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


Назад Оглавление Вперед

480

15.3.2.1. Множественное наследование и управление доступом, стр. 480

15.3.2.1. Множественное наследование и управление доступом, стр. 480

... допускаются некоторые ослабления по отношению к типу возвращаемого значения.

Следует отметить, что эти "некоторые ослабления" не являются простой формальностью. Рассмотрим следующий пример:

#include <iostream.h>

struct B1 {
       int b1;  // непустая
};

struct B2 {
       int b2;  // непустая
       virtual B2* vfun() {
               cout<<"B2::vfun()\n";  // этого мы не должны увидеть
               return this;
       }
};

struct D : B1, B2 {  // множественное наследование от непустых классов
       virtual D* vfun() {
               cout<<"D::vfun(): this="<<this<<"\n";
               return this;
       }
};

int main()
{
 D d;

 D* dptr=&d;
 cout<<"dptr\t"<<dptr<<"\n";

 void* ptr1=dptr->vfun();
 cout<<"ptr1\t"<<ptr1<<"\n";

 B2* b2ptr=&d;
 cout<<"b2ptr\t"<<b2ptr<<"\n";

 void* ptr2=b2ptr->vfun();
 cout<<"ptr2\t"<<ptr2<<"\n";
}

Обратите внимание: в данном примере я воспользовался "некоторыми ослаблениями" для типа возвращаемого значения D::vfun(), и вот к чему это привело:

dptr    0012FF6C
D::vfun(): this=0012FF6C
ptr1    0012FF6C
b2ptr   0012FF70
D::vfun(): this=0012FF6C
ptr2    0012FF70

Т.о. оба раза была вызвана D::vfun(), но возвращаемое ей значение зависит от способа вызова (ptr1!=ptr2), как это, собственно говоря, и должно быть. Делается это точно так же, как уже было описано, -- через автоматическую генерацию vfun_hack() и запись ее адреса в соответствующую часть vtbl. Только сама vfun_hack() "существенно" усложняется:

// псевдокод
B2* vfun_hack(B2 *const this)
{
 D* ptr= D::vfun(static_cast<D*>(this));

 return static_cast<B2*>(ptr);
}

Коль скоро мы должны обеспечить как пре- так и постобработку, вложенного вызова функции не избежать.

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

И раз уж речь зашла о виртуальных функциях с ковариантным типом возврата, стоит привести соответствующую часть стандарта:

10.3. Виртуальные функции (Virtual functions [class.virtual])

- 5 - Тип возвращаемого значения замещающей функции может быть или идентичен типу замещаемой функции или быть ковариантным (covariant). Если функция D::f замещает функцию B::f, типы возвращаемых ими значений будут ковариантными, если они удовлетворяют следующим условиям:

Если тип возвращаемого значения D::f отличается от типа возвращаемого значения B::f, то тип класса в возвращаемом значении D::f должен быть завершен в точке определения D::f или он должен быть типом D. Когда замещающая функция будет вызывана (как последняя заместившая функция), тип ее возвращаемого значения будет (статически) преобразован в тип возвращаемого значения замещаемой функции (см. expr.call). Например:

class B {};
class D : private B { friend class Derived; };
struct Base {
 virtual void vf1();
 virtual void vf2();
 virtual void vf3();
 virtual B*   vf4();
 virtual B*   vf5();
 void f();
};

struct No_good : public Base {
 D* vf4();  // ошибка: базовый класс B недоступен
};

class A;
struct Derived : public Base {
 void vf1();     // виртуальная и замещает Base::vf1()
 void vf2(int);  // не виртуальная, скрывает Base::vf2()
 char vf3();     // ошибка: неправильный тип возвращаемого значения
 D*   vf4();     // OK: возвращает указатель на производный класс
 A*   vf5();     // ошибка: возвращает указатель на незавершенный класс
 void f();
};

void g()
{
 Derived d;
 Base* bp=&d;      // стандартное преобразование: Derived* в Base*
 bp->vf1();        // вызов  Derived::vf1()
 bp->vf2();        // вызов  Base::vf2()
 bp->f();          // вызов  Base::f()  (не виртуальная)
 B* p=bp->vf4();   // вызов  Derived::pf() и преобразование
                   // возврата в B*
 Derived* dp=&d;
 D* q=dp->vf4();   // вызов  Derived::pf(), преобразование
                   // результата в B* не осуществляется
 dp->vf2();        // ошибка: отсутствует аргумент
}

А что означает загадочная фраза "меньшие cv-квалификаторы"?

3.9.3. CV-квалификаторы [basic.type.qualifier]

- 4 - Множество cv-квалификаторов является частично упорядоченным:

Нет cv-квалификатора

<

const

Нет cv-квалификатора

<

volatile

Нет cv-квалификатора

<

const volatile

Const

<

const volatile

Volatile

<

const volatile


Назад Оглавление Вперед

498

16.2.3. STL-контейнеры, стр. 498

16.2.3. STL-контейнеры, стр. 498

Она явилась результатом целенаправленного поиска бескомпромиссно эффективных общих алгоритмов.

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

Рассмотрим следующий пример:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <list>

struct List {  // односвязный список
       struct Data {
              int val;
              Data* next;

              Data(int v, Data* n=0) : val(v), next(n) {}
       };

       Data *head, *tail;

       List() { head=tail=0; }
       ~List() {
            for (Data *ptr=head, *n; ptr; ptr=n) {  // удаляем все элементы
                n=ptr->next;
                delete ptr;
            }
       }

       void push_back(int v) {  // добавляем элемент
         if (!head) head=tail=new Data(v);
         else tail=tail->next=new Data(v);
       }
};

long Count, Var;

void f1()
{
 List lst;
 for (int i=0; i<1000; i++)
     lst.push_back(i);

 for (List::Data* ptr=lst.head; ptr; ptr=ptr->next)
     Var+=ptr->val;
}

void f2()
{
 std::list<int> lst;
 for (int i=0; i<1000; i++)
     lst.push_back(i);

 for (std::list<int>::const_iterator ci=lst.begin(); ci!=lst.end(); ++ci)
     Var+=*ci;
}

int main(int argc,char** argv)
{
 if (argc>1) Count=atol(argv[1]);

 clock_t c1,c2;
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000; j++)
          f1();

  c2=clock();
  printf("f1(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000; j++)
          f2();

  c2=clock();
  printf("f2(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
}

В нем f1() использует определенный нами List: вставляет 1000 элементов, а затем проходит по списку.

Т.к. STL использует собственный распределитель памяти (вскоре вы увидите, что делает она это совсем не напрасно), то то же самое следует попробовать и нам:

struct List {  // односвязный список
       struct Data {
              int val;
              Data* next;

              Data(int v, Data* n=0) : val(v), next(n) {}

              // для собственного распределения памяти
              static Data* free;
              static void allocate();
              void* operator new(size_t);
              void operator delete(void*,size_t);
       };

       // . . .
};

List::Data* List::Data::free;

void List::Data::allocate()
{
 const int sz=100;  // выделяем блоки по sz элементов
 free=reinterpret_cast<Data*>(new char[sz*sizeof(Data)]);

 // сцепляем свободные элементы
 for (int i=0; i<sz-1; i++)
     free[i].next=free+i+1;
 free[sz-1].next=0;
}

inline void* List::Data::operator new(size_t)
{
 if (!free) allocate();

 Data* ptr=free;
 free=free->next;

 return ptr;
}

inline void List::Data::operator delete(void* dl,size_t)
{  // добавляем в начало списка свободных элементов
 Data* ptr=static_cast<Data*>(dl);
 ptr->next=free;
 free=ptr;
}

Обратите внимание, что в данном примере наш распределитель памяти не возвращает полученную память системе.

И, для чистоты эксперимента , в заключение попробуем двусвязный список -- его по праву можно назвать вручную написанной альтернативой std::list<int>:

struct DList {  // двусвязный список
       struct Data {
              int val;
              Data *prev, *next;

              Data(int v, Data* p=0, Data* n=0) : val(v), prev(p), next(n) {}

              // для собственного распределения памяти
              static Data* free;
              static void allocate();
              void* operator new(size_t);
              void operator delete(void*,size_t);
       };

       Data *head, *tail;

       DList() { head=tail=0; }
       ~DList() {
            for (Data *ptr=head, *n; ptr; ptr=n) {  // удаляем все элементы
                n=ptr->next;
                delete ptr;
            }
       }

       void push_back(int v) {  // добавляем элемент
         if (!head) head=tail=new Data(v);
         else tail=tail->next=new Data(v,tail);
       }
};

Итак, все готово, и можно приступать к тестированию. Данные три теста я попробовал на двух разных компиляторах, вот результат:

 

односвязный

односвязный с собственным распределителем памяти

двусвязный с собственным распределителем памяти

f1()

f2()

f1()

f2()

f1()

f2()

реализация 1

9.6

12.1

1.1

12.1

1.3

12.1

реализация 2

20.2

2.5

1.8

2.5

1.9

2.5

И что же мы здесь видим?

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


Назад Оглавление Вперед

505

16.3.4. Конструкторы, стр. 505

16.3.4. Конструкторы, стр. 505

То есть каждый из 10 000 элементов vr инициализируется конструктором Record(), а каждый из s1 элементов контейнера vi инициализируется int().

Инициализация 10 000 элементов конструктором по умолчанию не может не впечатлять -- только в очень редком случае нужно именно это. Если вы выделяете эти 10 000 элементов про запас, для последующей перезаписи, то стоит подумать о следующей альтернативе:

vector<X> vx;          // объявляем пустой вектор
vx.resize(10000);      // резервируем место воизбежание "дорогих"
                       // перераспределений в push_back()
// . . .
vx.push_back(x_work);  // добавляем элементы по мере надобности

О ней тем более стоит подумать, т.к. даже в отличной реализации STL 3.2 от SGI конструктор

vector<int> vi(s1);

подразумевает собой явный цикл заполнения нулями:

for (int i=0; i<s1; i++)
    vi.elements[i]=0;

и требуется достаточно интеллектуальный оптимизатор для превращения этого цикла в вызов memset()

memset(vi.elements, 0, sizeof(int)*s1);

что значительно улучшит производительность (конечно не программы вообще, а только данного отрезка кода). Matt Austern поставлен в известность, и в будущих версиях SGI STL можно ожидать повышения производительности данного конструктора.


Назад Оглавление Вперед

508

16.3.5. Операции со стеком, стр. 508

16.3.5. Операции со стеком, стр. 508

Сноска: То есть память выделяется с некоторым запасом (обычно на десять элементов). -- Примеч. ред.

Очень жаль, что дорогая редакция сочла возможным поместить в книгу такую глупость. Для приведения количества "дорогих" перераспределений к приемлемому уровню O(log(N)), в STL используется увеличение объема зарезервированной памяти в два раза, а при простом добавлении некоторого количества (10, например) мы, очевидно, получим O(N), что есть плохо. Также отмечу, что для уменьшения количества перераспределений стоит воспользоваться resize(), особенно, если вы заранее можете оценить предполагаемую глубину стека.


Назад Оглавление Вперед

541

17.4.1.2. Итераторы и пары, стр. 541

17.4.1.2. Итераторы и пары, стр. 541

Также обеспечена функция, позволяющая удобным образом создавать pair.

Честно говоря, при первом знакомстве с шаблонами от всех этих многословных объявлений начинает рябить в глазах, и не всегда понятно, что именно удобно в такой вот функции:

template <class T1,class T2>
pair<T1,T2> std::make_pair(const T1& t1, const T2& t2)
{
 return pair<T1,T2>(t1,t2);
}

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

char c=1;
int  i=2;

// пробуем создать "пару"
pair(c,i);            // неправильно -- pair<char,int> не выводится
pair<char,int>(c,i);  // правильно
make_pair(c,i);       // правильно

Назад Оглавление Вперед

543

17.4.1.5. Сравнения, стр. 543

17.4.1.5. Сравнения, стр. 543

Поэтому для константных ассоциативных массивов не существует версии operator[](), стр. 543

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


Назад Оглавление Вперед

555

17.5.3.3. Другие операции, стр. 555

17.5.3.3. Другие операции, стр. 555

К сожалению, вызов явно квалифицированного шаблона члена требует довольно сложного и редкого синтаксиса.

А вот тут мы имеем дело с откровенным "ляпом" в стандарте, увы! Во-первых, приведенную фразу следует читать так: К сожалению, вызов явно квалифицированного члена-шаблона всегда требует довольно сложного и редкого синтаксиса. Вот тут-то и скрывается "ляп"; если я могу написать так:

f<int>();  // f -- функция-шаблон

то почему это запрещено для функции-члена

obj.f<int>();           // ошибка: (obj.f)<(int) . . .
obj.template f<int>();  // правильно

Это нелогично, и действительно есть компиляторы (GNU C 2.95, например), способные правильно разбираться с явной квалификацией параметров при вызове члена-шаблона без использования "template". Д-р Страуструп пишет по этому поводу:

"template" is not always needed. A compiler can hold lots of information that it can use to disambiguate constructs. However, to ensure portability it is important not to rely on information that is not used by all compilers and tools.

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

Остается надеяться, что в стандарт внесут соответствующие изменения.


Назад Оглавление Вперед

556

17.6. Определение нового контейнера, стр. 556

17.6. Определение нового контейнера, стр. 556

... а потом применяйте поддерживаемый hash_map.

А вот еще один "ляп", и нет ему оправдания! Дело в том, что в стандарте понятия "поддерживаемый hash_map" не существует. Еще больше пикантности данной ситуации придает тот факт, что в самой STL, которая является основной частью стандартной библиотеки С++, hash_map есть (и есть уже давно). Д-р Страуструп пишет по этому поводу, что hash_map просто проглядели, а когда хватились, то было уже поздно -- никакие существенные изменения внести в стандарт уже было нельзя. Ну что ж, бывает...


Назад Оглавление Вперед

583

18.4.4.1. Связыватели, стр. 583

18.4.4.1. Связыватели, стр. 583

Читаемо? Эффективно?

Что же нам советуют признать читаемым и эффективным (впрочем, к эффективности претензий действительно нет; теоретически).

list<int>::const_iterator p=find_if(c.begin(),c.end(),bind2nd(less<int>(),7));

Осмелюсь предложить другой вариант:

for (list<int>::const_iterator p=c.begin(); p!=c.end(); ++p)
    if (*p<7) break;

Трудно ли это написать? По-видимому, нет. Является ли этот явный цикл менее читаемым? По моему мнению, он даже превосходит читаемость примера с использованием bind2nd(). А если нужно написать условие вида *p>=5 && *p<100, что, в принципе, встречается не так уж и редко, то вариант с использованием связывателей и find_if() проигрывает однозначно. Стоит добавить и чисто психологический эффект -- вызов красивой функции часто подсознательно воспринимается атомарной операцией и не лишне подчеркнуть, что, порой, за красивым фасадом скрывается крайне неэффективный последовательный поиск.

В целом, я агитирую против потери здравого смысла при использовании предоставленного нам пестрого набора свистулек и колокольчиков. Увы, следует признать, что для сколько-нибудь сложного применения они не предназначены, да и на простом примере польза практически не видна.


Назад Оглавление Вперед

584

18.4.4.2. Адаптеры функций-членов, стр. 584

18.4.4.2. Адаптеры функций-членов, стр. 584

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

Теперь немного про вызовы функций-членов для элементов контейнера с помощью механизма mem_fun(). Действительно, вариант

for_each(lsp.begin(),lsp.end(),mem_fun(&Shape::draw));  // рисуем все фигуры

подкупает своим изяществом. И даже более того, предоставляемые mem_fun() возможности действительно могут быть востребованы, например, при реализации некоторого абстрактного шаблона разработки (design pattern). Но за красивым фасадом скрывается вызов функции через указатель на член -- операция отнюдь не дешевая и далеко не все компиляторы умеют встраивать вызов функции через такой указатель, будем рисковать?

А что, если нам нужно повернуть все фигуры на заданный угол? bind2nd(), говорите? А если на разные углы да причем не все элементы контейнера, и эти углы рассчитываются по сложному алгоритму? По-моему, такой вариант в реальных программах встречается гораздо чаще.

Выходит, что и механизм mem_fun() не очень-то предназначен для серьезного использования. Изучить его, конечно, стоит, а вот использовать или нет -- решать вам.


Назад Оглавление Вперед

592

18.6. Алгоритмы, модифицирующие последовательность, стр. 592

18.6. Алгоритмы, модифицирующие последовательность, стр. 592

Вместо вставки и удаления элементов такие алгоритмы изменяют значения элементов...

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


Назад Оглавление Вперед

592a

18.6.1. Копирование, стр. 592

18.6.1. Копирование, стр. 592

Определения базовых операций копирования тривиальны...

Но в таком виде они будут совершенно неэффективны в приложении ко встроенным типам, ведь общеизвестно, что для копирования больших объемов информации (если без него действительно никак нельзя обойтись) следует использовать функции стандартной библиотеки С memcpy() и memmove(). Вы нечасто используете векторы встроенных типов? Осмелюсь заметить, что вектор указателей встречается не так уж и редко и как раз подходит под это определение. К счастью, у меня есть хорошая новость: в качественной реализации STL (например от SGI) вызов операции копирования для vector<int> как раз и приведет к эффективному memmove().

Выбор подходящего алгоритма производится на этапе компиляции с помощью специально определенного шаблона __type_traits<> -- свойства типа. Который (по умолчанию) имеет безопасные настройки для сложных типов с нетривиальными конструкторами/деструкторами и оптимизированные специализации для POD типов, которые можно копировать простым перемещением блоков памяти.

В С++ вы часто будете встречать аббревиатуру POD (Plain Old Data). Что же она обозначает? POD тип -- это тип, объекты которого можно безопасно перемещать в памяти (с помощью memmove(), например). Данному условию очевидно удовлетворяют встроенные типы (в том числе и указатели) и классы без определяемой пользователем операции присваивания и деструктора.

Почему я об этом говорю? Потому что, например, очевидное определение класса Date является POD типом:

class Date {
      int day, mon, year;
      // или даже
      long val;  // yyyymmdd
 public:
      // ...
};

Поэтому стоит разрешить оптимизацию предоставив соответствующую специализацию __type_traits<>:

template<> struct __type_traits<Date> {
 // ...
};

Только обратите внимание: __type_traits<> -- не часть стандартной библиотеки, разные реализации могут использовать различные имена или даже не производить оптимизацию вообще. Изучите то, что есть у вас.


Назад Оглавление Вперед

622

19.2.5. Обратные итераторы, стр. 622

19.2.5. Обратные итераторы, стр. 622

Это приводит к тому, что * возвращает значение *(current-1) ...

Да, по смыслу именно так:

24.4.1.3.3 - operator* [lib.reverse.iter.op.star]

 reference operator*() const;

-1- Effects:

 Iterator tmp = current;
 return *--tmp;

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

I don't think anyone would use a reverse iterator if an iterator was an alternative, but then you never know what people might know. When you actually need to go through a sequence in reverse order a reverse iterator is often quite efficient compared to alternatives. Finally, there may not be any overhead because where the iterator is a vector the temporary isn't hard to optimize into a register use. One should measure before worrying too much about overhead.

Я не думаю, что кто-то будет использовать обратный итератор, когда можно использовать обычный, но мы никогда не можем знать, что думают другие люди. Когда вам действительно нужно пройти последовательноть в обратном порядке, обратный итератор является вполне приемлемой альтернативой. В принципе, иногда можно избежать накладных расходов вообще, например в случае обратного прохода по вектору (когда итератором является просто T*). В любом случае, прежде чем беспокоиться о производительности, следует произвести реальные измерения.

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

И раз уж речь зашла о реальных измерениях, давайте их произведем.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <list>

long Count, Var;

std::list<int> lst;

void f1()
{
 typedef std::list<int>::reverse_iterator RI;
 for (RI p=lst.rbegin(); p!=lst.rend(); ++p)
     Var+=*p;
}

void f2()
{
 std::list<int>::iterator p=lst.end();
 if (p!=lst.begin())
    do {
       --p;
       Var+=*p;
    } while (p!=lst.begin());
}

int main(int argc,char** argv)
{
 if (argc>1) Count=atol(argv[1]);

 for (int i=0; i<10000; i++)
     lst.push_back(i);

 clock_t c1,c2;
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000; j++)
          f1();

  c2=clock();
  printf("f1(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
 {
  c1=clock();

  for (long i=0; i<Count; i++)
      for (long j=0; j<1000; j++)
          f2();

  c2=clock();
  printf("f2(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
 }
}

В данном примере список из 10 000 элементов проходится несколько тысяч раз (задается параметром) с использованием обратного (в f1()) и обычного (в f2()) итераторов. При использовании качественного оптимизатора разницы времени выполнения замечено не было, а для "обычных" реализаций она составила от 45% до 2.4 раза.

И еще одна проблема: приводит ли постинкремент итератора к существенным накладным расходам по сравнению с преинкрементом? Давайте внесем соответствующие изменения:

void f1()
{
 typedef std::list<int>::iterator I;
 for (I p=lst.begin(); p!=lst.end(); ++p)
     Var+=*p;
}

void f2()
{
 typedef std::list<int>::iterator I;
 for (I p=lst.begin(); p!=lst.end(); p++)
     Var+=*p;
}

И опять все тот же результат: разницы может не быть, а там, где она проявлялась, ее величина находилась в пределах 5 -- 30 процентов.

В целом, не стоит использовать потенциально более дорогие обратные итераторы и постинкременты, если вы не убедились в интеллектуальности используемого оптимизатора.


Назад Оглавление Вперед

637

19.4.2. Распределители памяти, определяемые пользователем, стр. 637

19.4.2. Распределители памяти, определяемые пользователем, стр. 637

template<class T>
T* Pool_alloc<T>::allocate(size_type n, void* =0)
{
 if (n==1) return static_cast<T*>(mem_alloc());
 // ...
}

Как всегда, самое интересное скрывается за многоточием. Как же нам реализовать часть allocate<>() для n!=1? Простым вызовом в цикле mem_alloc()? Увы, в данном случае очевидное решение не подходит совершенно. Почему? Давайте рассмотрим поведение Pool_alloc<char>. Глядя на конструктор оригинального Pool:

Pool::Pool(unsigned int sz)
      : esize(sz<sizeof(Link*) ? sizeof(Link*) : sz)
{
 // ...
}

можно заметить, что для sz==sizeof(char) для каждого char мы будем выделять sizeof(Link*) байт памяти. Для "обычной" реализации это означает четырехкратный перерасход памяти! Т.о. выделение памяти для массивов объектов типа X, где sizeof(X)<sizeof(Link*) становится нетривиальной задачей, равно как и последующее их освобождение в deallocate<>(), фактически, придется принципиально изменить алгоритм работы аллокатора.


Назад Оглавление Вперед

641

19.4.4. Неинициализированная память, стр. 641

19.4.4. Неинициализированная память, стр. 641

template<class T, class A> T* temporary_dup(vector<T,A>& v)
{
 T* p=get_temporary_buffer<T>(v.size()).first;
 if (p==0) return 0;
 copy(v.begin(),v.end(),raw_storage_iterator<T*,T>(p));
 return p;
}

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

template<class T, class A> T* temporary_dup(vector<T,A>& v)
{
 pair<T*,ptrdiff_t> p(get_temporary_buffer<T>(v.size()));

 if (p.second<v.size()) {
    if (p.first) return_temporary_buffer(p.first);
    return 0;
 }

 copy(v.begin(),v.end(),raw_storage_iterator<T*,T>(p));
 return p;
}

Назад Оглавление Вперед

647

20.2.1. Особенности символов, стр. 647

20.2.1. Особенности символов, стр. 647

Вызов assign(s,n,x) при помощи assign(s[i],x) присваивает n копий x строке s.
Функция compare() использует для сравнения символов lt() и eq().

К счастью, для обычных символов char_traits<char> это не так, в том смысле, что не происходит вызов в цикле lt(), eq(), assign(s[i],x), а используются специально для этого предназначенные memcmp() и memset(), что, впрочем, не влияет на конечный результат. Т.е. используя strcmp() мы ничего не выигрываем, даже более того, в специально проведенных мной измерениях производительности, сравнения string оказались на 30% быстрее, чем принятое в С сравнение char* с помощью strcmp(). Что и не удивительно: для string размеры сравниваемых массивов char известны заранее.


Назад Оглавление Вперед

652

20.3.4. Конструкторы, стр. 652

20.3.4. Конструкторы, стр. 652

Реализация basic_string хранит длину строки, не полагаясь на завершающий символ (ноль).

Вместе с тем, хорошо оптимизированные реализации хранят строку вместе с завершающим нулем, дабы максимально ускорить функцию basic_string::c_str(). Не секрет, что большинство используемых функций (традиционно) принимают строку в виде [const] char* вместо эквивалентного по смыслу [const] string&, исходя из того простого факта, что мы не можем ускорить "безопасную" реализацию, но можем скрыть эффективную за безопасным интерфейсом.

К слову сказать, исходя из моего опыта, слухи об опасности манипулирования простыми char* в стиле С оказываются сильно преувеличенными. Да, вы должны следить за всеми мелочами, но, например, ни у кого не возникает протеста по поводу того, что если в формуле корней квадратного уравнения мы вместо ‘-‘ напишем ‘+', то результат будет неверен.

Резюмируя данный абзац, хочу сказать, что string использовать можно и нужно, но если логика работы вашей программы интенсивно использует манипуляции со строками, стоит подумать о разработке собственных средств, основанных на функциях типа memcpy(), а в "узких" местах без этого просто не обойтись.


Назад Оглавление Вперед

655

20.3.6. Присваивание, стр. 655

20.3.6. Присваивание, стр. 655

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

Я бы попросил вас серьезно отнестись к данному совету (т.е. к проверке имеющейся реализации). Например, SGI STL 3.2 всегда копирует символы строки, не полагаясь на основанную на подсчете ссылок версию. Авторы библиотеки объясняют это тем, что использующие модель подсчета ссылок строки не подходят для многопоточных приложений, ими утверждается, что использующие данную реализацию строк многопоточные приложения аварийно завершают свою работу один раз в несколько месяцев и именно из-за строк. Мне, честно говоря, данное утверждение кажется несерьезным, но факт остается фактом -- существуют отлично оптимизированные реализации стандартной библиотеки, которые по тем или иным причинам отказались от использования основанных на подсчете ссылок строк.

Резюмируя данный материал хочу отметить, что я всегда, где это возможно, стараюсь избегать копирования строк, например путем передачи const string&.


Назад Оглавление Вперед

676

21.2.2. Вывод встроенных типов, стр. 676

21.2.2. Вывод встроенных типов, стр. 676

... будет интерпретировано так:

(cerr.operator<<("x=")).operator<<(x);

Конечно же на самом деле все не так: в новых потоках ввода-вывода оператор вывода строки больше не является функцией-членом, следовательно оно будет интерпретировано так:

 operator<<(cerr,"x=").operator<<(x);

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


Назад Оглавление Вперед

687

21.3.4. Ввод символов, стр. 687

21.3.4. Ввод символов, стр. 687

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

Вынужден вас огорчить -- потоки С++ не обладают данным важным свойством, они всегда работают медленнее С, а в некоторых реализациях -- медленно до смешного (правда, объективности ради стоит отметить, что мне попадались и совершенно отвратительно реализованные FILE* потоки С, в результате чего код С++ работал быстрее, но это просто недоразумение, если не сказать крепче!). Рассмотрим следующую программу:

#include <stdio.h>
#include <time.h>
#include <io.h>  // для open()
#include <fcntl.h>
#include <iostream.h>
#include <fstream.h>

void workc(char*);
void workcpp(char*);
void work3(char*);

int main(int argc, char **argv)
{
 if (argc==3)
    switch (*argv[2]-'0') {
           case 1: {
                workc(argv[1]);
                break;
           }
           case 2: {
                workcpp(argv[1]);
                break;
           }
           case 3: {
                work3(argv[1]);
                break;
           }
    }
}

void workc(char* fn)
{
 FILE* fil=fopen(fn,"rb");
 if (!fil) return;

 time_t t1; time(&t1);

 long count=0;
 while (getc(fil)!=EOF)
       count++;

 time_t t2; time(&t2);

 fclose(fil);
 cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ;
}

void workcpp(char* fn)
{
 // для старых реализаций: ifstream fil(fn,ios::in|ios::binary);
 ifstream fil(fn,ios_base::in|ios_base::binary);
 if (!fil) return;

 time_t t1; time(&t1);

 long count=0;
 while (fil.get()!=EOF)
       count++;

 time_t t2; time(&t2);
 cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ;
}

class File {
      int            fd;           // дескриптор файла
      unsigned char  buf[BUFSIZ];  // буфер стандартного размера
      unsigned char* gptr;         // следующий читаемый символ
      unsigned char* gend;         // конец данных

      int uflow();
 public:
      File(char* fn) : gptr(0), gend(0) { fd=open(fn,O_RDONLY|O_BINARY); }
      ~File() { if (Ok()) close(fd); }

      int Ok() { return fd!=-1; }

      int gchar() { return (gptr<gend) ? *gptr++ : uflow(); }
};

int File::uflow()
{
 if (!Ok()) return EOF;

 int rd=read(fd,buf,BUFSIZ);
 if (rd<=0) {  // ошибка или EOF
    close(fd);
    fd=-1;

    return EOF;
 }

 gptr=buf;
 gend=buf+rd;

 return *gptr++;
}

void work3(char* fn)
{
 File fil(fn);
 if (!fil.Ok()) return;

 time_t t1; time(&t1);

 long count=0;
 while (fil.gchar()!=EOF)
       count++;

 time_t t2; time(&t2);

 cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ;
}

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

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

А сейчас попробуем получить информацию к размышлению -- проведем серию контрольных запусков и посмотрим на результат. И что же нам говорят безжалостные цифры? На отлично оптимизированных реализациях мы получим разницу "всего" в 40% (например, 8 сек. против 11), а вот на "обычных"... Для одного широко распространенного коммерческого пакета (не будем показывать пальцем) я даже получил отличие в 11 раз! (7 сек. против 79).

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

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

#define getc(f) \
  ((--((f)->level) >= 0) ? (unsigned char)(*(f)->curp++) : \
    _fgetc (f))

Т.е. коротенький макрос вместо функции. Как говорится -- всего-ничего. А вот для С++ все гораздо сложнее. Вот код из оптимизированной реализации-чемпиона:

inline
int get() { if (!ipfx1()) return EOF;
  else { int ch = _strbuf->sbumpc();
         if (ch == EOF) set(ios::eofbit);
         return ch;
       } }

Проследим вызовы функций при обычном поведении:

inline
int ipfx1() { // Optimized version of ipfx(1).
 if (!good()) { set(ios::failbit); return 0; }
 else {
   _IO_flockfile(_strbuf);
   if (_tie && rdbuf()->in_avail() == 0) _tie->flush();
   return 1;
 }
    }
// _IO_getc() -- это похожая на getc() работа с буфером
inline int sbumpc() { return _IO_getc(this); }

Даже при беглом взгляде становится ясно, что количество выполняемого кода весьма сильно превосходит то, что мы имеем в С. Но самое интересное заключается в том, что количество "лишнего" кода существенно превышает показанную разницу в 40%. В чем же дело? А дело в том, что узким местом данного цикла является обращение к диску, и именно из-за стоимости дисковых операций получаются эти неожиданные 40%. Более подробно данная тема будет рассмотрена в посвященном оптимизации кода разделе.

А что будет, если мы выделим большой буфер и зададим его нашим функциям?

void workc(char* fn)
{
 FILE* fil=fopen(fn,"rb");
 if (!fil) return;

 if (Bsize>0 && setvbuf(fil,Buf,_IOFBF,Bsize)) return;

 // . . .
}

void workcpp(char* fn)
{
 // для старых реализаций: ifstream fil(fn,ios::in|ios::binary);
 ifstream fil(fn,ios_base::in|ios_base::binary);
 if (!fil) return;

 // для старых реализаций: fil.rdbuf()->setbuf(Buf,Bsize);
 if (Bsize>0) fil.rdbuf()->pubsetbuf(Buf,Bsize);

 // . . .
}

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

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

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


Назад Оглавление Вперед

701

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

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

Коль скоро с эффективностью потоков ввода-вывода мы уже разобрались, следует поговорить об удобстве. К сожалению, для сколько-нибудь сложного форматирования предоставляемые потоками средства не предназначены. Не в том смысле, что средств нет, а в том, что данные средства совершенно неудобны и легко выводят из себя привыкшего к элегантному формату ...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

Думаю, что комментарии излишни.

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

cout<<c_form(day,"02")<<'.'<<c_form(mon,"02")<<'.'<<year<<'\n';

Вот исходный код:

#include <iostream>
#include <ctype.h>

class c_formBase {  // класс базовых операций
 protected:
      // для компактной записи
      typedef std::ios_base::fmtflags                          fmt_t;
      typedef std::basic_ostream<char,std::char_traits<char> > ostr_t;
      typedef std::ios_base                                    ios;
 private:
      const char* form;   // формат
      int   arg1, arg2;   // аргументы ширины/точности
      fmt_t       flags;  // сохраняемые флаги
      int         width;  // ширина
      int         prec;   // точность
      char        fill;   // символ-заполнитель
      fmt_t       myfl;   // устанавливаемые флаги
 protected:
      // конструктор с разбором формата
      // формат: [-|0] [число|*] [.[число|*]] [e|f|g|o|x]
      c_formBase( const char* form_,
                  const int arg1_,
                  const int arg2_
                )
      : form(form_), arg1(arg1_), arg2(arg2_) {
        const char* iptr=form;  // текущий символ строки формата

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

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

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

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

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

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

      static int getval(const char*& iptr) {  // чтение числа
             int ret=0;
             do ret=ret*10+ *iptr -'0';
                while (isdigit(*++iptr));

             return ret;
      }

      void prologue(ostr_t& os) {  // настройка
           flags=os.flags();
           // очищаем floatfield и устанавливаем свои флаги
           os.flags((flags & ~ios::floatfield) | myfl);

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

      void epilogue(ostr_t& os) {  // восстановление
           os.flags(flags);

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

template <class T>
class c_formClass : private c_formBase {  // класс-шаблон для вывода типа T
      T val;  // выводимое значение
 public:
      c_formClass( T val_,
                   const char* form,
                   const int arg1,
                   const int arg2
                 )
      : c_formBase(form,arg1,arg2), val(val_) { }

      // оператор вывода
      friend ostr_t& operator<<(ostr_t& os, const c_formClass& cf_) {
             c_formClass& cf=const_cast<c_formClass&>(cf_);

             cf.prologue(os);
             os<<cf.val;
             cf.epilogue(os);

             return os;
      }
};

template <class T>  // функция-помощник
inline c_formClass<T> c_form( T val,             // выводимое значение
                              const char* form,  // формат вывода
                              const int arg1=0,  // необязательные
                              const int arg2=0   // параметры
                             )
{ return c_formClass<T>(val,form,arg1,arg2); }

Принцип его работы основан на следующей идее: функция c_form<>() возвращает объект класса c_formClass<>, для которого определена операция вывода в ostream.

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

cout<<c_form<int>(day,"02");

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

И еще. Несмотря на то, что конструктор c_formBase возбуждает исключения (std::invalid_argument), я не стал специфицировать это явным образом. Дело в том, что большинство компиляторов не умеет встраивать функции с явно заданной спецификацией возбуждаемых исключений, а одной из целей создания нашего манипулятора являлась производительнотсь.

Для старых реализаций приходится использовать другой код, что, кстати сказать, позволяет нам реально ощутить некоторые удобные изменения, привнесенные в С++ стандартом:

#include <iostream.h>
#include <ctype.h>

class c_formBase {  // класс базовых операций
 private:
      const char* form;   // формат
      int   arg1, arg2;   // аргументы ширины/точности
      long        flags;  // сохраняемые флаги
      int         width;  // ширина
      int         prec;   // точность
      char        fill;   // символ-заполнитель
      long        myfl;   // устанавливаемые флаги
 protected:
      // конструктор с разбором формата
      // формат: [-|0] [число|*] [.[число|*]] [e|f|g|o|x]
      c_formBase( const char* form_,
                  const int arg1_,
                  const int arg2_
                )
      : form(form_), arg1(arg1_), arg2(arg2_) {
        const char* iptr=form;  // текущий символ строки формата

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

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

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

        prec=0;
        if (*iptr=='.') {  // есть точность
           if (*++iptr=='*') {
              prec=arg1;
              iptr++;
           }
           else if (isdigit(*iptr)) prec=getval(iptr);
        }

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

      static int getval(const char*& iptr) {  // чтение числа
             int ret=0;
             do ret=ret*10+ *iptr -'0';
                while (isdigit(*++iptr));

             return ret;
      }

      void prologue(ostream& os) {  // настройка
           flags=os.flags();
           // очищаем floatfield и устанавливаем свои флаги
           os.flags((flags & ~ios::floatfield) | myfl);

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

      void epilogue(ostream& os) {  // восстановление
           os.flags(flags);

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

template <class T>
class c_formClass : private c_formBase {  // класс-шаблон для вывода типа T
      T val;  // выводимое значение
 public:
      c_formClass( T val_,
                   const char* form,
                   const int arg1,
                   const int arg2
                 )
      : c_formBase(form,arg1,arg2), val(val_) { }

      // оператор вывода
      friend ostream& operator<<(ostream& os, const c_formClass<T>& cf_) {
             c_formClass<T>& cf=(c_formClass<T>&) cf_;

             cf.prologue(os);
             os<<cf.val;
             cf.epilogue(os);

             return os;
      }
};

template <class T>  // функция-помощник (два аргумента)
inline c_formClass<T> c_form(T val, char* form, int arg1, int arg2)
{ return c_formClass<T>(val,form,arg1,arg2); }

template <class T>  // функция-помощник (один аргумент)
inline c_formClass<T> c_form(T val, char* form, int arg1)
{ return c_formClass<T>(val,form,arg1,0); }

template <class T>  // функция-помощник (без аргументов)
inline c_formClass<T> c_form(T val, char* form)
{ return c_formClass<T>(val,form,0,0); }

Назад Оглавление Вперед

711

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

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

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

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

streamsize readsome(char_type* s, streamsize n);

Эффект: Если !good() вызвать setstate(failbit), которая может возбудить исключение. Иначе извлечь символы и поместить их в массив, на который указывает s. Если rdbuf()->in_avail() == -1, вызвать setstate(eofbit) (которая может возбудить исключение ios_base::failure (lib.iostate.flags)) и не извлекать символы;

Возвращает: Количество прочитанных символов.


Назад Оглавление Вперед

773

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

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

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

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

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

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


Назад Оглавление Вперед

879

А.5. Выражения, стр. 879

А.5. Выражения, стр. 879

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

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

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. Друзья, стр. 931

B.13.2. Друзья, стр. 931

Приведенный в конце страницы пример нужно заменить на:

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.


Назад Оглавление Вперед

notes1

Оптимизация

Оптимизация

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

  1. Программа должна делать то, что от нее требуется.
  2. Она должна делать это хорошо.

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

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

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

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

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

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

  1. работа с внешними устройствами
  2. системные вызовы
  3. вызовы собственных функций
  4. локальные управляющие структуры

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

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

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

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

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

С++ как и С при вызове функции размещает параметры в стеке. Т.е. имея параметр в регистре, компилятор заносит его в стек, вызывает функцию, а в теле функции опять переносит параметр в регистр. Всего этого можно избежать использовав соответствующее соглашение вызова (в некоторых реализациях используется зарезервированное слово _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() и вызывающая функции еще какого-либо файла; и пошло-поехало. А когда каждое приложение содержит свыше полусотни исходных файлов, а в проекте несколько сотен приложений, то навести порядок постфактум уже не представляется возможным.


Назад Оглавление Вперед

notes2

Макросы

Макросы

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

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

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

  1. Макрос как надъязыковое средство. Хороший примером является простой, но удивительно полезный отладочный макрос _VAL_, выводящий имя и значение переменной:
  2. #define _VAL_(var) #var "=" << var << " "
    

    Надъязыковой является работа с переменной, как с текстом, путем перевода имени переменной (оно существует только в исходном коде программы) в строковый литерал, реально существующий в коде бинарном. Данную возможность могут предоставить только макросы.

  3. Информация о текущем исходном файле и строке -- ее пользу при отладке трудно переоценить. Для этого я использую специальный макрос _ADD_. Например:
  4.  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__ является числовым, а не строковым литералом и чтобы привести его к нужному типу приходится проявить некоторую смекалку. Мы, очевидно, не можем сразу написать #__LINE__, т.к. результатом будет "__LINE__". Из-за этого мы передаем __LINE__ другому макросу -- _ADD_tmp_, который уже увидит значение __LINE__ (34, например), и в следующей итерации мы можем использовать #arg без всяких неожиданностей.

  5. Получение значения числового макроса в виде строки. Как показывает практика, данное действие необходимо не только в виде части сложных макросов. Допустим, что для взаимодействия с SQL-сервером у нас определен класс DB::Query с соответствующей функцией
  6. 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
    

    Кстати, приведенный пример наглядно демонстрирует невозможность полностью эквивалентной замены всех числовых макросов на принятые в С++

    const int FieldOK=7;
    enum { FieldOK=7 };
    

    макрос _GETSTR_ с ними не сможет работать.

  7. Многократно встречающиеся части кода. Рассмотрим еще один пример из области работы с SQL-сервером. Предположим, что нам нужно выбрать данные из некоторой таблицы. Это можно сделать влоб:
  8. 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 содержит запятые. К сожалению, данное печальное ограничение в примитивном препроцессоре С++ никак нельзя обойти.

  9. Многократно встречающиеся подобные части кода. Представим себе, что мы пишем приложение для банковской сферы и должны выбрать информацию по некоторым счетам. В России, например, счет состоит из многих полей, которые для удобства работы собирают в специальную структуру, а в таблице он может быть представлен смежными полями с одинаковым префиксом:
  10. 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;
    // . . .
    

    Думаю, что комментарии излишни.

  11. Рассмотрим более тонкий пример подобия. Пусть нам потребовалось создать таблицу для хранения часто используемой нами структуры данных:
struct A {
       MyDate Date;
       int    Field2;
       short  Field3;
};

Мы не можем использовать идентификатор Date для имени столбца таблицы, т.к. DATE является зарезервированным словом SQL. Эта проблема легко обходится с помощью приписывания некоторого префикса:

struct TableA {
       DB::Date  xDate;
       DB::Int   xField2;
       DB::Short xField3;

       void Clear();
       TableA& operator=(A&);
};

А теперь определим функции-члены:

void TableA::Clear()
{
 xDate="";
 xField2="";
 xField3="";
}

TableA& TableA::operator=(A& a)
{
 xDate=ToDB(a.Date);
 xField2=ToDB(a.Field2);
 xField3=ToDB(a.Field3);

 return *this;
}

Гарантирую, что если TableA содержит хотя бы пару-тройку десятков полей, то написание подобного кода вам очень быстро наскучит, мягко говоря! Нельзя ли это сделать один раз, а потом использовать результаты? Оказывается можно:

void TableA::Clear()
{
#define CLR(arg) arg=""
 CLR(xDate);
 CLR(xField2);
 CLR(xField3);
#undef CLR
}

TableA& TableA::operator=(A& a)
{
// используем склейку лексем: ##
#define ASS(arg) x##arg=ToDB(a.arg);
 ASS(xDate);
 ASS(xField2);
 ASS(xField3);
#undef ASS

 return *this;
}

Теперь определение TableA::operator=() по TableA::Clear() не несет никакой нудной работы, если, конечно, ваш текстовый редактор поддерживает команды поиска и замены. Так же просто можно определить и обратное присваивание: A& A::operator=(TableA&).

Надеюсь, что после приведенных выше примеров вы по-новому посмотрите на роль макросов в С++.


Назад Оглавление Вперед

Last-modified: Mon, 01 May 2000 18:04:58 GMT
World LibraryРеклама в библиотекеБиблиотека не предназначена для детей! Проект Либмонстра, партнеры БЦБ - Украинская цифровая библиотека и Либмонстр Россия https://database.library.by