; } 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()
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; }что, в принципе, тоже корректно, только крайне неэффективно: при каждом проходе цикла создается временный объект, к которому затем применяется операция вызова функции.
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); }и выбор перегруженной функции осуществляется по обычным правилам. В общем, никакой мистики.
Вместе с тем, данная терминология совершенно естественна в теоретико-множественном смысле. А именно: каждый объект производного класса является объектом базового класса, а обратное, вообще говоря, неверно. Т.о. базовый класс шире, поэтому он и суперкласс. Путаница возникает из-за того, что больше сам класс, а не его объекты, которые ввиду большей общности класса должны иметь меньше особенностей (членов).
Это, вообще говоря, неверно. При применении множественного наследования "просто косвенного вызова" оказывается недостаточно. Рассмотрим следующую программу:
#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
содержит разные записи для каждого из вариантов вызова, так что решение, очевидно, есть. На практике, чаще всего, используется один из следующих способов:
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 трансляторов.
ptr
(если это вообще нужно):
vfun_entry_0: // ... // собственно код vfun // ... return; vfun_entry_1: ptr+=delta_1; // корректируем значение ptr goto vfun_entry_0; // и переходим к телу vfunВ этом случае
vtbl
содержит только адреса соответствующих точек входа и никаких напрасных вычислений не требуется. Специфическим недостатком данного способа является невозможность его реализации средствами ANSI C.
Потому что строковый литерал -- это объект с внутренней компоновкой (internal linkage).
М-да... Определенно, не самое удачное место русского перевода. Тем более, что в оригинале все предельно просто и понятно:
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.Как ни странно, конструктор-шаблон никогда не используется для генерации конструктора копирования, т.е. без явно определенного конструктора копирования будет сгенерирован конструктор копирования по умолчанию.
Далее хочу отметить, что постоянно встречающуюся в переводе фразу "конструктор шаблона" следует понимать как "конструктор-шаблон".
Если вы решили, что тем самым должна повыситься производительность, ввиду того, что в теле функции отсутствуют блоки 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%, что с практической точки зрения почти эквивалентно полному отсутствию расходов.
Но это в лучших реализациях! О том, что происходит в реализациях "обычных" лучше не упоминать -- как говорит герой известного анекдота: "Гадкое зрелище"...
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
не является обычным "умным" указателем, однако он прекрасно справляется с предоставленной ему ролью -- обеспечивать безопасную относительно исключений работу с автоматическими указателями, и делать это без существенных накладных расходов.
new
Т.к. приведенные в книге объяснения немного туманны, вот соответствующая часть стандарта:
5.3.4. New [expr.new]
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
:
// ключевое слово nothrow отсутствует: // f() разрешено возбуждать любые исключения прямо или косвенно void f() { // ... }
// f() запрещено возбуждать любые исключения прямо или косвенно, // проверяется на этапе компиляции void f() nothrow { // ... }
void f() { // здесь можно возбуждать исключения прямо или косвенно nothrow { // nothrow-блок // код, находящийся в данном блоке никаких исключений возбуждать // не должен, проверяется на этапе компиляции } // здесь снова можно возбуждать исключения }
std::bad_exception
описанным в данном разделе образом. Вот что об этом пишет д-р Страуструп:
The standard doesn't support the mapping of exceptions as I describe it in 14.6.3. It specifies mapping tostd::bad_exception
for exceptions thrown explicitly within anunexpected()
function. This makesstd::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]
void unexpected();сразу же после завершения раскрутки стека (stack unwinding).
unexpected()
не может вернуть управление, но может (пере)возбудить исключение. Если она возбуждает новое исключение, которое разрешено нарушенной до этого спецификацией исключений, то поиск подходящего обработчика будет продолжен с точки вызова сгенерировавшей неожиданное исключение функции. Если же она возбудит недозволенное исключение, то: Если спецификация исключений не содержит класс std::bad_exception
(18.6.2.1), то будет вызвана terminate()
, иначе (пере)возбужденное исключение будет заменено на определяемый реализацией объект типа std::bad_exception
и поиск соответствующего обработчика будет продолжен описанным выше способом.
std::bad_exception
, то любое неописанное исключение может быть заменено на std::bad_exception
внутри unexpected()
.
class XX : B { /* ... */ }; // B -- закрытый базовый класс class YY : B { /* ... */ }; // B -- открытая базовая структура
На самом деле, в оригинале было так:
class XX : B { /* ... */ }; // B -- закрытая база struct YY : B { /* ... */ }; // B -- открытая базаТ.е. вне зависимости от того, является ли база
B
классом или структурой, права доступа к унаследованным членам определяются типом наследника: по умолчанию, класс закрывает доступ к своим унаследованным базам, а структура -- открывает.
В принципе, в этом нет ничего неожиданного -- доступ по умолчанию к обычным, не унаследованным, членам задается теми же правилами.
Тут, конечно, имеет место досадная опечатка, что, кстати сказать, сразу видно из приведенного примера. Т.е. читать следует так: ... если он разрешен по некоторому из возможных путей.
Это утверждение, вообще говоря, неверно и я вам советую никогда так не поступать. Сейчас покажу почему.
Прежде всего, стоит отметить, что в 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*
, каждая из реализаций постаралась отличиться в выборе представления указателей на члены, особенно первая. Что же мы можем сказать еще?
Указатели на функции-члены во втором компиляторе реализованы неоптимально, т.к. иногда они содержат указатель на "обычную" функцию (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
, и генерации функций-заглушек (со всеми вытекающими потерями производительности) было бы можно избежать. Более того, его, как и указатель на данные-член, всегда можно было бы передавать в другое адресное пространство.
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. "Распределители памяти, определяемые пользователем" было бы некорректным.
Именно так. Т.е. если вы объявили деструктор некоторого класса
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
имеет виртуальный деструктор, это в конечном итоге приведет к вызову соответствующего метода.
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 была первой, кто предложил операторы для размещения и освобождения массивов.
Следует отметить, что эти "некоторые ослабления" не являются простой формальностью. Рассмотрим следующий пример:
#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