2* b2ptr=&d; printf("b2ptr\t%p\n", b2ptr); void* ptr2=b2ptr->vfun(); printf("ptr2\t%p\n", ptr2); } Обратите внимание: в данном примере я воспользовался "некоторыми ослаблениями" для типа возвращаемого значения D::vfun(), и вот к чему это привело:
dptr    0012FF6C
D::vfun(): this=0012FF6C
ptr1    0012FF6C
b2ptr   0012FF70
D::vfun(): this=0012FF6C
ptr2    0012FF70
Т.о. оба раза была вызвана D::vfun(), но возвращаемое ей значение зависит от способа вызова (ptr1!=ptr2), как это, собственно говоря, и должно быть.

Делается это точно так же, как уже было описано в разделе 361 "12.2.6. Виртуальные функции", только помимо корректировки принимаемого значения this необходимо дополнительно произвести корректировку this возвращаемого. Понятно, что виртуальные функции с ковариантным типом возврата встречаются настолько редко, что реализация их вызова посредством расширения vtbl вряд ли может быть признана адекватной. На практике обычно создаются специальные функции-заглушки, чьи адреса помещаются в соответствующие элементы vtbl:

// псевдокод

// оригинальная D::vfun, написанная программистом
D* D::vfun(D *const this)
{
 // ...
}

// сгенерированная компилятором функция-заглушка для вызова D::vfun() через
// указатель на базовый класс B2
B2* D::vfun_stub(B2 *const this)
{
 return D::vfun(this+delta_1)+delta_2;
}
где возвращаемый функцией указатель корректируется посредством константы delta_2, вообще говоря, не равной delta_1.

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

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

  1. Тип возвращаемого значения замещающей функции может быть или идентичен типу замещаемой функции или быть ковариантным (covariant). Если функция D::f замещает функцию B::f, типы возвращаемых ими значений будут ковариантными, если они удовлетворяют следующим условиям: Если тип возвращаемого значения D::f отличается от типа возвращаемого значения B::f, то тип класса в возвращаемом значении D::f должен быть завершен в точке определения D::f или он должен быть типом D. Когда замещающая функция будет вызывана (как последняя заместившая функция), тип ее возвращаемого значения будет (статически) преобразован в тип возвращаемого значения замещаемой функции (5.2.2). Например:
    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 (базовый класс D) недоступен
    };
    
    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]

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

    нет cv-квалификатора < const
    нет cv-квалификатора < volatile
    нет cv-квалификатора < const volatile
    const < const volatile
    volatile < const volatile


Стр.498: 16.2.3. STL-контейнеры

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

Вместе с тем, не стоит думать, что 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()
{
 typedef std::list<int> list_type;

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

 for (list_type::const_iterator ci=lst.begin(), cend=lst.end(); ci!=cend; ++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 {

              // ...

              // для собственного распределения памяти
              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;
}
Обратите внимание, что в данном примере наш распределитель памяти не возвращает полученную память системе. Но это не memory leak (утечка памяти) -- это memory pool, т.е. заранее выделенный запас памяти для быстрого последующего использования. На первый взгляд, разница между memory leak и memory pool может показаться слишком тонкой, но она есть: дело в том, что в первом случае потребление памяти не ограничено, вплоть до полного ее исчерпания, а во втором оно никогда не превысит реально затребованного программой объема плюс некоторая дельта, не превосходящая размер выделяемого блока.

И еще, наш распределитель содержит очень серьезную ошибку -- он неправильно обрабатывает удаление нуля (NULL-указателя). В нашем примере это не имеет значения, но в реальном коде вы обязаны это учесть, т.е.:

inline void List::Data::operator delete(void* dl, size_t)
{
 if (!dl) return;  // игнорируем NULL

 // добавляем в начало списка свободных элементов
 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. Конструкторы

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

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

vector<X> vx;          // объявляем пустой вектор
vx.reserve(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. Операции со стеком

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

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


Стр.526: 17.1.4.1. Сравнения

Таким образом, при использовании в качестве ключей C-строк ассоциативные контейнеры будут работать не так, как ожидало бы большинство людей.

И дело не только в определении операции "меньше", а еще и в том, что char* не стоит использовать в качестве элементов STL контейнеров вообще: контейнер будет содержать значение указателя -- не содержимое строки, как кто-то по наивности мог полагать. Например, следующая функция содержит серьезную ошибку:

void f(set<char*>& cset)
{
 for (;;) {
     char word[100];

     // считываем слово в word ...

     cset.insert(word);  // ошибка: вставляем один и тот же указатель
                         // на локальную переменную
 }
}
Для получения ожидаемого результата следует использовать string:
void f(set<string>& cset)
{
 for (;;) {
     char word[100];

     // считываем слово в word ...

     cset.insert(word);  // OK: вставляем string
 }
}
Использование char* в STL контейнерах приводит к чрезвычайно коварным ошибкам, т.к. иногда все работает правильно. Например документация к sgi STL широко использует char* в своих учебных примерах:
struct ltstr
{
  bool operator()(const char* s1, const char* s2) const
  {
    return strcmp(s1, s2) < 0;
  }
};

int main()
{
  const int N = 6;
  const char* a[N] = {"isomer", "ephemeral", "prosaic",
                      "nugatory", "artichoke", "serif"};

  set<const char*, ltstr> A(a, a + N);

  // и т.д.
}
Данный пример вполне корректен, но стоит только вместо статически размещенных строковых литералов использовать локально формируемые C-строки, как неприятности не заставят себя ждать.

Относитесь скептически к учебным примерам!


Стр.541: 17.4.1.2. Итераторы и пары

Также обеспечена функция, позволяющая удобным образом создавать 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.3. Индексация

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

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


Стр.555: 17.5.3.3. Другие операции

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

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

В самом деле, если разрешено

f<int>();  // f -- функция-шаблон
то почему вдруг компилятор не может правильно разобраться с
obj.f<int>();  // f -- функция-шаблон, член класса
Может, и разбирается!

Исторически, непонимание возникло из-за того, что:

  1. непосредственно этот туманный аспект использования квалификатора template был изобретен комитетом по стандартизации, а не д-ром Страуструпом;
  2. первым компилятором, поддерживающим экспериментальные (на тот момент) нововведения, был aC++ от HP. Данный компилятор ошибочно требовал наличия квалификатора, что, вкупе с неочевидным текстом стандарта, не могло не ввести в заблуждение.
Дальнейшее развитие темы "сложного и редкого синтаксиса" можно найти в разделе B.13.6. template как квалификатор.

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

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

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


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

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

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

list<int>::const_iterator p=find_if(c.begin(),c.end(),bind2nd(less<int>(),7));
Осмелюсь предложить другой вариант:
list<int>::const_iterator p;
for (p=c.begin(); p!=c.end(); ++p)
    if (*p<7) break;
Трудно ли это написать? По-видимому, нет. Является ли этот явный цикл менее читаемым? По моему мнению, он даже превосходит читаемость примера с использованием bind2nd(). А если нужно написать условие вида *p>=5 && *p<100, что, в принципе, встречается не так уж и редко, то вариант с использованием связывателей и find_if() проигрывает однозначно. Стоит добавить и чисто психологический эффект: вызов красивой функции часто подсознательно воспринимается атомарной операцией и не лишне подчеркнуть, что за красивым фасадом порой скрывается крайне неэффективный последовательный поиск.

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


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

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

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

for_each(lsp.begin(),lsp.end(),mem_fun(&Shape::draw));  // рисуем все фигуры
подкупает своим изяществом. И даже более того, предоставляемые mem_fun() возможности действительно могут быть востребованы, например, при реализации некоторого абстрактного шаблона разработки (design pattern). Но за красивым фасадом скрывается вызов функции через указатель на член -- операция отнюдь не дешевая и далеко не все компиляторы умеют встраивать вызов функции через такой указатель. Будем рисковать?

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

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


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

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

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

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


Стр.592: 18.6.1. Копирование

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

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

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

В C++ вы часто будете встречать аббревиатуру 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. Обратные итераторы

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

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

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

reference operator*() const;
  1. Действия:
    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.

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

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

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

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

long Count, Var;

typedef std::list<int> list_type;
list_type lst;

void f1()
{
 for (list_type::reverse_iterator ri=lst.rbegin(), rend=lst.rend(); ri!=rend;
      ++ri)
     Var+=*ri;
}

void f2()
{
 list_type::iterator i=lst.end(), beg=lst.begin();
 if (i!=beg) {
    do {
       --i;
       Var+=*i;
    } while (i!=beg);
 }
}

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()
{
 for (list_type::iterator i=lst.begin(), end=lst.end(); i!=end; ++i)
     Var+=*i;
}

void f2()
{
 for (list_type::iterator i=lst.begin(), end=lst.end(); i!=end; i++)
     Var+=*i;
}
И опять все тот же результат: разницы может не быть, а там, где она проявлялась, ее величина находилась в пределах 5 - 30 процентов.

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


Стр.634: 19.4.1. Стандартный распределитель памяти

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

Вполне резонным будет вопрос: что же здесь имелось ввиду? Недостаток каких свойств мешает ссылкам C++ быть "совершенными"? Д-р. Страуструп ответил следующее:

Something that would allow a copy constructor to be defined using a user-defined reference object.

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


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

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. Неинициализированная память

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.first;
}

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

Вызов 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% быстрее, чем принятое в C сравнение char* с помощью strcmp(). Что и не удивительно: для string размеры сравниваемых массивов char известны заранее.


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

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

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

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

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


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

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

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

Ими утверждается, что использующие данную реализацию строк многопоточные приложения аварийно завершают свою работу один раз в несколько месяцев и именно из-за строк. В принципе, модель подсчета ссылок действительно плохо подходит для многопоточных приложений, т.к. ее использование приводит к существенным накладным расходам (более подробно об этом можно почитать у Herb Sutter Reference Counting - Part III), но вот собственно аварийное завершение работы может быть вызвано только ошибками в реализации -- чудес не бывает.

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

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


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

... будет интерпретировано так:
(cerr.operator<<("x=")).operator<<(x);

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

operator<<(cerr,"x=").operator<<(x);
Товарищи программисты! Еще раз повторю: никогда не копируйте блоками старый текст, а если это все-таки необходимо, -- обязательно проверяйте каждую загогулину!

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


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

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

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

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

using namespace std;

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_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* bend;         // конец данных

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

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

      int gchar() { return (gptr<bend) ? *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;
 bend=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" ;
}
Ее нужно запускать с дву