; } 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. Вызов функции

Функция, которая вызывается повторно, -- это 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

Обратите внимание, что для неконстантного объекта, 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. Производные классы

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

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


Стр.361: 12.2.6. Виртуальные функции

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

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

#include <stdio.h>

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

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

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

int main()
{
 D d;

 D* dptr=&d;
 printf("dptr\t%p\n", dptr);
 dptr->vfun();

 B2* b2ptr=&d;
 printf("b2ptr\t%p\n", b2ptr);
 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() всегда сводился бы к вызову (*vtbl[index_of_vfun])(ptr), то в нашей программе мы бы получили b2ptr==0x283feec==this!=0x283fee8.

Вопрос номер два: как они это делают? Суть проблемы в том, что одна и та же замещенная виртуальная функция (D::vfun() в нашем случае) может быть вызвана как через указатель на производный класс (ptr==0x283fee8) так и через указатель на один из базовых классов (ptr==0x283feec), чьи значения не совпадают, в то время как переданное значение this должно быть одним и тем же (this==0x283fee8) в обоих случаях.

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

  1. В таблицу vtbl добавляется дополнительная колонка -- vdelta. Тогда в процессе вызова виртуальной функции кроме адреса функции из vtbl извлекается и дельта, чье значение добавляется к ptr:
    addr=vtbl[index].vaddr;    // извлекаем адрес функции vfun
    delta=vtbl[index].vdelta;  // извлекаем дельту, зависящую от способа вызова vfun
    (*addr)(ptr+delta);        // вызываем vfun
    Существенным недостатком данного способа является заметное увеличение размеров vtbl и значительные накладные расходы времени выполнения: дело в том, что абсолютное большинство вызовов виртуальных функций не требует коррекции значения ptr, так что соответствующие им значения vdelta будут нулевыми. Достоинством -- возможность вызова виртуальной функции из ANSI C кода, что важно для C++ -> C трансляторов.
  2. Более эффективным решением является создание нескольких точек входа для одной и той же виртуальной функции, каждая из которых соответствующим образом корректирует значение ptr (если это вообще нужно):
    vfun_entry_0:
      // ...
      // собственно код vfun
      // ...
      return;
    
    vfun_entry_1:
      ptr+=delta_1;       // корректируем значение ptr
      goto vfun_entry_0;  // и переходим к телу vfun
    В этом случае vtbl содержит только адреса соответствующих точек входа и никаких напрасных вычислений не требуется. Специфическим недостатком данного способа является невозможность его реализации средствами ANSI C.
Интересное и достаточно подробное описание представления объектов и реализации механизма вызова виртуальных функций можно найти в статье C++: Under the Hood. Она описывает реализацию, использованную разработчиками MSVC.

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

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

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


Стр.399: 13.6.2. Члены-шаблоны

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

М-да... Определенно, не самое удачное место русского перевода. Тем более, что в оригинале все предельно просто и понятно:

Curiously enough, a template constructor is never used to generate a copy constructor, so without the explicitly declared copy constructor, a default copy constructor would have been generated.

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

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


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

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

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

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

void ResourceAcquire();
void ResourceRelease();
void Work();

struct RAII {
       RAII()  { ResourceAcquire(); }
       ~RAII() { ResourceRelease(); }
};

void f1()
{
 ResourceAcquire();

 try { Work(); }
 catch (...) {
       ResourceRelease();
       throw;
 }

 ResourceRelease();
}

void f2()
{
 RAII raii;
 Work();
}

long Var, Count;

void ResourceAcquire() { Var++; }
void ResourceRelease() { Var--; }
void Work() { Var+=2; }

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

Как же работают исключения? Если вкратце, то в разных реализациях исключения работают по-разному. И всегда чрезвычайно нетривиально! Особенно много сложностей возникает с ОС, использующими так называемый Structured Exception Handling и/или поддерживающими многопоточность (multithreading). Фактически, с привычными нам современными ОС...

На текущий момент в Internet можно найти достаточное количество материала по реализации exception handling (EH) в C++ и не только, приводить здесь который не имеет особого смысла. Тем не менее, влияние EH на производительность C++ программ заслуживает отдельного обсуждения.

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

Но более надежным (и, кстати, не зависящим от способа реализации EH) опровержением исходной посылки является тот факт, что EH добавляет дополнительные дуги в Control Flow Graph, т.е. в граф потоков управления, что не может не сказаться на возможностях оптимизаци.

Тем не менее, накладные расходы на EH в лучших реализациях не превышают 5%, что с практической точки зрения почти эквивалентно полному отсутствию расходов.

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


Стр.421: 14.4.2. auto_ptr

В стандартном заголовочном файле <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

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

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

5.3.4. New [expr.new]

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

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

Спецификация исключений не является частью типа функции, и 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.

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

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

Имеющийся на текущий момент опыт свидетельствует о том, что критически важной для написания exception safe кода информацией является ответ на вопрос: Может ли функция вообще возбуждать исключения? Эта информация известна уже на этапе компиляции и может быть проверена без особого труда.

Так, например, можно ввести ключевое слово nothrow:

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

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

В настоящее время стандарт не поддерживает отображение исключений в 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. Я возбудил соответствующее issue в комитете по стандартизации.

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

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().

Стр.460: 15.3.2. Доступ к базовым классам

class XX : B { /* ... */ };  // B -- закрытый базовый класс
class YY : B { /* ... */ };  // B -- открытая базовая структура

На самом деле, в оригинале было так:

class XX : B { /* ... */ };  // B -- закрытая база
struct YY : B { /* ... */ };  // B -- открытая база
Т.е. вне зависимости от того, является ли база B классом или структурой, права доступа к унаследованным членам определяются типом наследника: по умолчанию, класс закрывает доступ к своим унаследованным базам, а структура -- открывает.

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


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

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

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


Стр.475: 15.5. Указатели на члены

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

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

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

struct S {
       int i;
       void f();
};

void g()
{
 cout<<&S::i;  // ошибка: operator<< не реализован для типа int S::*
 cout<<&S::f;  // ошибка: operator<< не реализован для типа void (S::*)()
}
Это довольно странно. Andrew Koenig пишет по этому поводу, что дело не в недосмотре разработчиков библиотеки ввода/вывода, а в том, что не существует переносимого способа для вывода чего-либо содержательного (кстати, я оказался первым, кто вообще об этом спросил, так что проблему определенно нельзя назвать злободневной). Мое же мнение состоит в том, что каждая из реализаций вполне способна найти способ для вывода более-менее содержательной информации, т.к. в данном случае даже неидеальное решение -- это гораздо лучше, чем вообще ничего.

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

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

struct S {
       int i1;
       int i2;

       void f1();
       void f2();

       virtual void vf1();
       virtual void vf2();
};

const int SZ=sizeof(&S::f1);

union {
      unsigned char c[SZ];
      int i[SZ/sizeof(int)];
      int S::* iptr;
      void (S::*fptr)();
} hack;

void printVal(int s)
{
 if (s%sizeof(int)) for (int i=0; i<s; i++) printf(" %02x", hack.c[i]);
 else for (int i=0; i<s/sizeof(int); i++)
          printf(" %0*x", sizeof(int)*2, hack.i[i]);

 printf("\n");
 memset(&hack, 0, sizeof(hack));
}

int main()
{
 printf("sizeof(int)=%d sizeof(void*)=%d\n", sizeof(int), sizeof(void*));

 hack.iptr=&S::i1;
 printf("sizeof(&S::i1 )=%2d value=", sizeof(&S::i1));
 printVal(sizeof(&S::i1));

 hack.iptr=&S::i2;
 printf("sizeof(&S::i2 )=%2d value=", sizeof(&S::i2));
 printVal(sizeof(&S::i2));

 hack.fptr=&S::f1;
 printf("sizeof(&S::f1 )=%2d value=", sizeof(&S::f1));
 printVal(sizeof(&S::f1));

 hack.fptr=&S::f2;
 printf("sizeof(&S::f2 )=%2d value=", sizeof(&S::f2));
 printVal(sizeof(&S::f2));

 hack.fptr=&S::vf1;
 printf("sizeof(&S::vf1)=%2d value=", sizeof(&S::vf1));
 printVal(sizeof(&S::vf1));

 hack.fptr=&S::vf2;
 printf("sizeof(&S::vf2)=%2d value=", sizeof(&S::vf2));
 printVal(sizeof(&S::vf2));
}

void S::f1() {}
void S::f2() {}

void S::vf1() {}
void S::vf2() {}
Существенными для понимания местами здесь являются объединение hack, используемое для преобразования значения указателей на члены в последовательность байт (или целых), и функция printVal(), печатающая данные значения.

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

sizeof(int)=4 sizeof(void*)=4
sizeof(&S::i1 )= 8 value= 00000005 00000000
sizeof(&S::i2 )= 8 value= 00000009 00000000
sizeof(&S::f1 )=12 value= 004012e4 00000000 00000000
sizeof(&S::f2 )=12 value= 004012ec 00000000 00000000
sizeof(&S::vf1)=12 value= 004012d0 00000000 00000000
sizeof(&S::vf2)=12 value= 004012d8 00000000 00000000

sizeof(int)=4 sizeof(void*)=4
sizeof(&S::i1 )= 4 value= 00000001
sizeof(&S::i2 )= 4 value= 00000005
sizeof(&S::f1 )= 8 value= ffff0000 004014e4
sizeof(&S::f2 )= 8 value= ffff0000 004014f4
sizeof(&S::vf1)= 8 value= 00020000 00000008
sizeof(&S::vf2)= 8 value= 00030000 00000008

sizeof(int)=4 sizeof(void*)=4
sizeof(&S::i1 )= 4 value= 00000004
sizeof(&S::i2 )= 4 value= 00000008
sizeof(&S::f1 )= 4 value= 00401140
sizeof(&S::f2 )= 4 value= 00401140
sizeof(&S::vf1)= 4 value= 00401150
sizeof(&S::vf2)= 4 value= 00401160
Прежде всего в глаза бросается то, что несмотря на одинаковый размер int и void*, каждая из реализаций постаралась отличиться в выборе представления указателей на члены, особенно первая. Что же мы можем сказать еще?
  1. Во всех трех реализациях указатель на член-данные является смещением -- не прямым адресом. Это вполне логично и из этого следует, что эти указатели можно безопасно передавать из одного адресного пространства в другое.
  2. Указатели на невиртуальные функции члены являются или просто указателем на функцию, или содержат такой указатель в качестве одного из фрагментов. Очевидно, что их передавать в другое адресное пространство нельзя. Впрочем, в этом также нет ничего неожиданного.
  3. А теперь самое интересное -- указатели на виртуальные функции-члены. Как вы можете видеть, только у одного из трех компиляторов они получились похожими на "передаваемые" -- у второго.
Итак, указатели на виртуальные функции-члены можно безопасно передавать в другое адресное пространство чрезвычайно редко. И это правильно! Дело в том, что в определение C++ закралась ошибка: указатели на обычные и виртуальные члены должны быть разными типами. Только в этом случае можно обеспечить оптимальность реализации.

Указатели на функции-члены во втором компиляторе реализованы неоптимально, т.к. иногда они содержат указатель на "обычную" функцию (ffff0000 004014e4), а иногда -- индекс виртуальной функции (00020000 00000008). В результате чего, вместо того, чтобы сразу произвести косвенный вызов функции, компилятор проверяет старшую часть первого int, и если там стоит -1 (ffff), то он имеет дело с обычной функцией членом, иначе -- с виртуальной. Подобного рода проверки при каждом вызове функции-члена через указатель вызывают ненужные накладные расходы.

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

Например, следующий код:

struct S {
       virtual void vf() { /* 1 */ }
               void f () { /* 2 */ }
};

void g(void (S::*fptr)(), S* sptr)
{
 (sptr->*fptr)();
}

int main()
{
 S s;
 g(S::vf, &s);
 g(S::f , &s);
}
превращается в псевдокод:
void S_vf(S *const this) { /* 1 */ }
void S_f (S *const this) { /* 2 */ }

void S_vf_stub(S *const this)
{
 // виртуальный вызов функции S::vf()
 (this->vptr[index_of_vf])(this);
}

void g(void (*fptr)(S *const), S* sptr)
{
 fptr(sptr);
}

int main()
{
 S s;
 g(S_vf_stub, &s);  // обратите внимание: не S_vf !!!
 g(S_f      , &s);
}
А если бы в C++ присутствовал отдельный тип "указатель на виртуальную функцию-член", он был бы представлен простым индексом виртуальной функции, т.е. фактически простым size_t, и генерации функций-заглушек (со всеми вытекающими потерями производительности) было бы можно избежать. Более того, его, как и указатель на данные-член, всегда можно было бы передавать в другое адресное пространство.

Стр.477: 15.6. Свободная память

// полагаем, что p указывает на s байтов памяти, выделенной Employee::operator new()

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

Запомните: определяя operator delete(), вы обязаны правильно обрабатывать удаление нулевого указателя! Т.о. код должен выглядеть следующим образом:

void Employee::operator delete(void* p, size_t s)
{
 if (!p) return;  // игнорируем нулевой указатель

 // полагаем, что p указывает на s байтов памяти, выделенной
 // Employee::operator new() и освобождаем эту память
 // для дальнейшего использования
}
Интересно отметить, что стандартом специально оговорено, что аргумент p функции
template <class T> void std::allocator::deallocate(pointer p, size_type n);
не может быть нулевым. Без этого замечания использование функции Pool::free в разделе 19.4.2. "Распределители памяти, определяемые пользователем" было бы некорректным.

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

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

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

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 имеет виртуальный деструктор, это в конечном итоге приведет к вызову соответствующего метода.

Стр.479: 15.6.1. Выделение памяти под массив

На этом пункте стоит остановиться поподробнее, так как с оператором new[] связаны не вполне очевидные вещи. Не мудрствуя лукаво, привожу перевод раздела 10.3 "Array Allocation" из книги "The Design and Evolution of C++" одного известного автора:

Определенный для класса X оператор X::operator new() используется исключительно для размещения одиночных объектов класса X (включая объекты производных от X классов, не имеющих собственного распределителя памяти). Следовательно

X* p = new X[10];
не вызывает X::operator new(), т.к. X[10] является массивом, а не объектом класса X.

Это вызывало много жалоб, т.к. я не разрешил пользователям контролировать размещение массивов типа X. Однако я был непреклонен, т.к. массив элементов типа X -- это не объект типа X, и, следовательно, распределитель памяти для X не может быть использован. Если бы он использовался и для распределения массивов, то автор X::operator new() должен был бы иметь дело как с распределением памяти под объект, так и под массив, что сильно усложнило бы более распространенный случай. А если распределение памяти под массив не очень критично, то стоит ли вообще о нем беспокоиться? Тем более, что возможность управления размещением одномерных массивов, таких как X[d] не является достаточной: что, если мы захотим разместить массив X[d][d2]?

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

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

class X {
      // ...
      void* operator new(size_t sz);    // распределение объектов
      void operator delete(void* p);

      void* operator new[](size_t sz);  // распределение массивов
      void operator delete[](void* p);
};
Распределитель памяти для массивов используется для массивов любой размерности. Как и в случае других распределителей, работа operator new[] состоит в предоставлении запрошенного количества байт; ему не нужно самому беспокоиться о размере используемой памяти. В частности, он не должен знать о размерности массива или количестве его элементов. Laura Yaker из Mentor Graphics была первой, кто предложил операторы для размещения и освобождения массивов.

Стр.480: 15.6.2. "Виртуальные конструкторы"

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

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

#include <stdio.h>

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

struct B2 {
       int b2;  // непустая

       virtual B2* vfun()
       {
        printf("B2::vfun()\n");  // этого мы не должны увидеть
        return this;
       }
};

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

int main()
{
 D d;

 D* dptr=&d;
 printf("dptr\t%p\n", dptr);

 void* ptr1=dptr->vfun();
 printf("ptr1\t%p\n", ptr1);

 B