© Copyright Сергей Деревяго, 2000
Версия исправленная и дополненная, 12 Oct 2004
Origin: http://ders.angen.net/cpp3/
---------------------------------------------------------------
Сергей Деревяго
C++ 3rd: комментарии
Введение | |
43 | 1.3.1. Эффективность и структура |
73 | 2.5.5. Виртуальные функции |
79 | 2.7.2. Обобщенные алгоритмы |
128 | 5.1.1. Ноль |
192 | 7.4. Перегруженные имена функций |
199 | 7.6. Неуказанное количество аргументов |
202 | 7.7. Указатель на функцию |
296 | 10.4.6.2. Члены-константы |
297 | 10.4.7. Массивы |
316 | 11.3.1. Операторы-члены и не-члены |
328 | 11.5.1. Поиск друзей |
333 | 11.7.1. Явные конструкторы |
337 | 11.9. Вызов функции |
344 | 11.12. Класс String |
351 | 12.2. Производные классы |
361 | 12.2.6. Виртуальные функции |
382 | 13.2.3. Параметры шаблонов |
399 | 13.6.2. Члены-шаблоны |
419 | 14.4.1. Использование конструкторов и деструкторов |
421 | 14.4.2. auto_ptr |
422 | 14.4.4. Исключения и оператор new |
431 | 14.6.1. Проверка спецификаций исключений |
431 | 14.6.3. Отображение исключений |
460 | 15.3.2. Доступ к базовым классам |
461 | 15.3.2.1. Множественное наследование и управление доступом |
475 | 15.5. Указатели на члены |
477 | 15.6. Свободная память |
478 | 15.6. Свободная память |
479 | 15.6.1. Выделение памяти под массив |
480 | 15.6.2. "Виртуальные конструкторы" |
498 | 16.2.3. STL-контейнеры |
505 | 16.3.4. Конструкторы |
508 | 16.3.5. Операции со стеком |
526 | 17.1.4.1. Сравнения |
541 | 17.4.1.2. Итераторы и пары |
543 | 17.4.1.3. Индексация |
555 | 17.5.3.3. Другие операции |
556 | 17.6. Определение нового контейнера |
583 | 18.4.4.1. Связыватели |
584 | 18.4.4.2. Адаптеры функций-членов |
592 | 18.6. Алгоритмы, модифицирующие последовательность |
592 | 18.6.1. Копирование |
622 | 19.2.5. Обратные итераторы |
634 | 19.4.1. Стандартный распределитель памяти |
637 | 19.4.2. Распределители памяти, определяемые пользователем |
641 | 19.4.4. Неинициализированная память |
647 | 20.2.1. Особенности символов |
652 | 20.3.4. Конструкторы |
655 | 20.3.6. Присваивание |
676 | 21.2.2. Вывод встроенных типов |
687 | 21.3.4. Ввод символов |
701 | 21.4.6.3. Манипуляторы, определяемые пользователем |
711 | 21.6.2. Потоки ввода и буфера |
773 | 23.4.3.1. Этап 1: выявление классов |
879 | А.5. Выражения |
931 | B.13.2. Друзья |
935 | B.13.6. template как квалификатор |
Оптимизация | |
Макросы | |
Исходный код |
оффициальной странице.
Также не помешает ознакомиться с классической STL, ведущей начало непосредственно от Алекса Степанова. И, главное, не забудьте заглянуть к самому Бьерну Страуструпу.
Кстати, если вы еще не читали "The C programming Language" by Brian W. Kernighan and Dennis M. Ritchie, 2е издание, то я вам советую непременно это сделать -- Классика!
С уважением, Сергей Деревяго.
исходном коде текст программы, как правило, разнесен по нескольким файлам для предотвращения агрессивного выбрасывания "мертвого кода" качественными оптимизаторами):
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
struct A {
A();
~A();
};
void ACon();
void ADes();
void f1()
{
A a;
}
void f2()
{
ACon();
ADes();
}
long Var, Count;
A::A() { Var++; }
A::~A() { Var++; }
void ACon() { Var++; }
void ADes() { Var++; }
int main(int argc,char** argv)
{
if (argc>1) Count=atol(argv[1]);
clock_t c1,c2;
{
c1=clock();
for (long i=0; i<Count; i++)
for (long j=0; j<1000000; j++)
f1();
c2=clock();
printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
{
c1=clock();
for (long i=0; i<Count; i++)
for (long j=0; j<1000000; j++)
f2();
c2=clock();
printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
}
В ней функции
f1()
и f2()
делают одно и то же, только первая неявно, с помощью конструктора и деструктора класса A
, а вторая с помощью явного вызова ACon()
и ADes()
.
Для работы программа требует одного параметра -- сколько миллионов раз вызывать тестовые функции. Выберите значение, позволяющее f1()
работать несколько секунд и посмотрите на результат для f2()
.
При использовании качественного оптимизатора никакой разницы быть не должно; тем не менее, на некоторых платформах она определенно есть и порой достигает 10 раз!
А что же inline
? Давайте внесем очевидные изменения:
struct A {
A() { Var++; }
~A() { Var++; }
};
void f1()
{
A a;
}
void f2()
{
Var++;
Var++;
}
Теперь разницы во времени работы
f1()
и f2()
не быть должно. К несчастью, на большинстве компиляторов она все же присутствует.
Что же происходит? Наблюдаемый нами эффект называется abstraction penalty, т.е. обратная сторона абстракции или налагаемое на нас некачественными компиляторами наказание за использование (объектно-ориентированных) абстракций.
Давайте посмотрим как abstraction penalty проявляется в нашем случае.
Что же из себя представляет
void f1()
{
A a;
}
эквивалентное
void f1() // псевдокод
{
A::A();
A::~A();
}
И чем оно отличается от простого вызова двух функций:
void f2()
{
ACon();
ADes();
}
В данном случае -- ничем! Но, давайте рассмотрим похожий пример:
void f1()
{
A a;
f();
}
void f2()
{
ACon();
f();
ADes();
}
Как вы думаете, эквивалентны ли данные функции? Правильный ответ -- нет, т.к.
f1()
представляет собойvoid f1() // псевдокод
{
A::A();
try {
f();
}
catch (...) {
A::~A();
throw;
}
A::~A();
}
Т.е. если конструктор успешно завершил свою работу, то языком гарантируется, что обязательно будет вызван деструктор. Т.е. там, где создаются некоторые объекты, компилятор специально вставляет блоки обработки исключений для гарантии вызова соответствующих деструкторов. А накладные расходы в оригинальной
f1()
чаще всего будут вызваны присутствием ненужных в данном случае блоков обработки исключений (фактически, присутствием "утяжеленных" прологов/эпилогов):void f1() // псевдокод
{
A::A();
try {
// пусто
}
catch (...) {
A::~A();
throw;
}
A::~A();
}
Дело в том, что компилятор обязан корректно обрабатывать все возможные случаи, поэтому для упрощения компилятора его разработчики часто не принимают во внимание "частные случаи", в которых можно не генерировать ненужный код. Увы, подобного рода упрощения компилятора очень плохо сказываются на производительности интенсивно использующего средства абстракции и
inline
функции кода. Хорошим примером подобного рода кода является STL, чье использование, при наличии плохого оптимизатора, вызывает чрезмерные накладные расходы.
Поэкспериментируйте со своим компилятором для определения его abstraction penalty -- гарантированно пригодится при оптимизации "узких мест".
Стр.73: 2.5.5. Виртуальные функции
Требования по памяти составляют один указатель на каждый объект класса с виртуальными функциями, плюс одна
vtbl
для каждого такого класса.
На самом деле первое утверждение неверно, т.е. объект полученный в результате множественного наследования от полиморфных классов будет содержать несколько "унаследованных" указателей на vtbl
.
Рассмотрим следующий пример. Пусть у нас есть полиморфный (т.е. содержащий виртуальные функции) класс B1
:
struct B1 { // я написал struct чтобы не возиться с правами доступа
int a1;
int b1;
virtual ~B1() { }
};
И пусть имеющаяся у нас реализация размещает
vptr
(указатель на таблицу виртуальных функций класса) перед объявленными нами членами. Тогда данные объекта класса B1
будут расположены в памяти следующим образом:vptr_1 // указатель на vtbl класса B1
a1 // объявленные нами члены
b1
Если теперь объявить аналогичный класс
B2
и производный класс D
struct D: B1, B2 {
virtual ~D() { }
};
то его данные будут расположены следующим образом:
vptr_d1 // указатель на vtbl класса D, для B1 здесь был vptr_1
a1 // унаследованные от B1 члены
b1
vptr_d2 // указатель на vtbl класса D, для B2 здесь был vptr_2
a2 // унаследованные от B2 члены
b2
Почему здесь два
vptr
? Потому, что была проведена оптимизация, иначе их было бы три.
Я, конечно, понял, что вы имели ввиду: "Почему не один"? Не один, потому что мы имеем возможность преобразовывать указатель на производный класс в указатель на любой из базовых классов. При этом, полученный указатель должен указывать на корректный объект базового класса. Т.е. если я напишу:
D d;
B2* ptr=&d;
то в нашем примере
ptr
укажет в точности на vptr_d2
. А собственным vptr
класса D
будет являться vptr_d1
. Значения этих указателей, вообще говоря, различны. Почему? Потому что у B1
и B2
в vtbl
по одному и тому же индексу могут быть расположены разные виртуальные функции, а D
должен иметь возможность их правильно заместить. Т.о. vtbl
класса D
состоит из нескольких частей: часть для B1
, часть для B2
и часть для собственных нужд.
Подводя итог, можно сказать, что если мы используем множественное наследование от большого числа полиморфных классов, то накладные расходы по памяти могут быть достаточно существенными.
Судя по всему, от этих расходов можно отказаться, реализовав вызов виртуальной функции специальным образом, а именно: каждый раз вычисляя положение vptr
относительно this
и пересчитывая индекс вызываемой виртуальной функции в vtbl
. Однако это спровоцирует существенные расходы времени выполнения, что неприемлемо.
И раз уж так много слов было сказано про эффективность, давайте реально измерим относительную стоимость вызова виртуальной функции.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
struct B {
void f();
virtual void vf();
};
struct D : B {
void vf(); // замещаем B::vf
};
void f1(B* ptr)
{
ptr->f();
}
void f2(B* ptr)
{
ptr->vf();
}
long Var, Count;
void B::f() { Var++; }
void B::vf() { }
void D::vf() { Var++; }
int main(int argc,char** argv)
{
if (argc>1) Count=atol(argv[1]);
clock_t c1,c2;
D d;
{
c1=clock();
for (long i=0; i<Count; i++)
for (long j=0; j<1000000; j++)
f1(&d);
c2=clock();
printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
{
c1=clock();
for (long i=0; i<Count; i++)
for (long j=0; j<1000000; j++)
f2(&d);
c2=clock();
printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
}
В зависимости от компилятора и платформы, накладные расходы на вызов виртуальной функции составили от 10% до 2.5 раз. Т.о. можно утверждать, что "виртуальность" небольших функций может обойтись сравнительно дорого.
И слово "небольших" здесь не случайно, т.к. уже даже тест с функцией Аккермана (отлично подходящей для выявления относительной стоимости вызова)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
struct B {
int ackf(int x, int y);
virtual int vackf(int x, int y);
};
struct D : B {
int vackf(int x, int y); // замещаем B::vackf
};
void f1(B* ptr)
{
ptr->ackf(3, 5); // 42438 вызовов!
}
void f2(B* ptr)
{
ptr->vackf(3, 5); // 42438 вызовов!
}
int B::ackf(int x, int y)
{
if (x==0) return y+1;
else if (y==0) return ackf(x-1, 1);
else return ackf(x-1, ackf(x, y-1));
}
int B::vackf(int x, int y) { return 0; }
int D::vackf(int x, int y)
{
if (x==0) return y+1;
else if (y==0) return vackf(x-1, 1);
else return vackf(x-1, vackf(x, y-1));
}
long Count;
int main(int argc,char** argv)
{
if (argc>1) Count=atol(argv[1]);
clock_t c1,c2;
D d;
{
c1=clock();
for (long i=0; i<Count; i++)
for (long j=0; j<1000; j++)
f1(&d);
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(&d);
c2=clock();
printf("f2(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK);
}
}
показывает заметно другие результаты, существенно уменьшая относительную разность времени выполнения.
Стр.79: 2.7.2. Обобщенные алгоритмы
Встроенные в C++ типы низкого уровня, такие как указатели и массивы, имеют соответствующие операции, поэтому мы можем записать:
char vc1[200];
char vc2[500];
void f()
{
copy(&vc1[0],&vc1[200],&vc2[0]);
}
Ну, если к делу подойти формально, то записать мы так не можем. Вот что говорит об этом д-р Страуструп:
The issue is whether taking the address of one-past-the-last element of an array is conforming C and C++. I could make the example clearly conforming by a simple rewrite:
copy(vc1,vc1+200,vc2);
However, I don't want to introduce addition to pointers at this point of the book. It is a surprise to most experienced C and C++ programmers that&vc1[200]
isn't completely equivalent tovc1+200
. In fact, it was a surprise to the C committee also and I expect it to be fixed in the upcoming revision of the standard. (also resolved for C9x - bs 10/13/98).
Суть вопроса в том, разрешено ли в C и C++ взятие адреса элемента, следующего за последним элементом массива. Я мог сделать пример очевидно корректным простой заменой:
copy(vc1,vc1+200,vc2);
Однако, я не хотел вводить сложение с указателем в этой части книги. Даже для самых опытных программистов на C и C++ большим сюрпризом является тот факт, что&vc1[200]
не полностью эквивалентноvc1+200
. Фактически, это оказалось неожиданностью и для C комитета, и я ожидаю, что это недоразумение будет устранено в следующих редакциях стандарта.
Так в чем же нарушается эквивалентность? По стандарту C++ мы имеем следующие эквивалентные преобразования:
&vc1[200] -> &(*((vc1)+(200))) -> &*(vc1+200)
Действительно ли равенство
&*(vc1+200) == vc1+200
неверно?
It is false in C89 and C++, but not in K&R C or C9x. The C89 standard simply said that&*(vc1+200)
means dereferencevc1+200
(which is an error) and then take the address of the result, and the C++ standard copiled the C89 wording. K&R C and C9x say that&*
cancels out so that&*(vc1+200) == vc2+200
.
Это неверно в С89 и C++, но не в K&R C или С9х. Стандарт С89 говорит, что&*(vc1+200)
означает разыменованиеvc1+200
(что является ошибкой) и затем взятие адреса результата. И стандарт C++ просто взял эту формулировку из С89. Однако K&R C и С9х устанавливают, что&*
взаимно уничтожаются, т.е.&*(vc1+200) == vc1+200
.
Спешу вас успокоить, что на практике в выражении &*(vc1+200)
некорректное разыменование *(vc1+200)
практически никогда не произойдет, т.к. результатом всего выражения является адрес и ни один серьезный компилятор не станет выбирать значение по некоторому адресу (операция разыменования) чтобы потом получить тот же самый адрес с помощью операции &
.
Стр.128: 5.1.1. Ноль
Если вы чувствуете, что просто обязаны определить
NULL
, воспользуйтесьconst int NULL=0;
Суть данного совета в том, что согласно определению языка не существует контекста, в котором (определенное в заголовочном файле) значение NULL
было бы корректным, в то время как просто 0
-- нет.
Исходя из того же определения, передача NULL
в функции с переменным количеством параметров вместо корректного выражения вида static_cast<SomeType*>(0)
запрещена.
Безусловно, все это правильно, но на практике NULL
в функции с переменным количеством параметров все же передают. Например, так:
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
void error(int stat ...)
{
va_list ap;
va_start(ap, stat);
while (const char* sarg=va_arg(ap, const char *))
printf("%s", sarg);
va_end(ap);
exit(stat);
}
int main()
{
error(1, "Случилось ", "страшное", NULL); // внимание, ошибка!
// вместо NULL нужно использовать
// static_cast<const char *>(0)
}
Именно для поддержки подобного рода практики (некорректной, но широко распространенной) реализациям разрешено определять
NULL
как 0L
(а не просто 0
) на архитектурах, где sizeof(void*)==sizeof(long)>sizeof(int)
.Стр.192: 7.4. Перегруженные имена функций
Процесс поиска подходящей функции из множества перегруженных заключается в...
Приведенный в книге пункт [2] нужно заменить на:
Соответствие, достигаемое "продвижением" ("повышением в чине") интегральных типов (например,bool
вint
,char
вint
,short
вint;
ї B.6.1),float
вdouble
.
Также следует отметить, что доступность функций-членов не влияет на процесс поиска подходящей функции, например:
struct A {
private:
void f(int);
public:
void f(...);
};
void g()
{
A a;
a.f(1); // ошибка: выбирается A::f(int), использование
// которой в g() запрещено
}
Отсутствие данного правила породило бы тонкие ошибки, когда выбор подходящей функции зависел бы от места вызова: в функции-члене или в обычной функции.
Стр.199: 7.6. Неуказанное количество аргументов
До выхода из функции, где была использована
va_start()
, необходимо осуществить вызов va_end()
. Причина состоит в том, что va_start()
может модифицировать стек таким образом, что станет невозможен нормальный выход из функции.
Ввиду чего возникают совершенно незаметные подводные камни.
Общеизвестно, что обработка исключения предполагает раскрутку стека. Следовательно, если в момент возбуждения исключения функция изменила стек, то у вас гарантированно будут неприятности.
Таким образом, до вызова va_end()
следует воздерживаться от потенциально вызывающих исключения операций. Специально добавлю, что ввод/вывод C++ может генерировать исключения, т.е. "наивная" техника вывода в std::cout
до вызова va_end()
чревата неприятностями.
Стр.202: 7.7. Указатель на функцию
Причина в том, что разрешение использования
cmp3
в качестве аргумента ssort()
нарушило бы гарантию того, что ssort()
вызовется с аргументами mytype*
.
Здесь имеет место досадная опечатка, совершенно искажающая смысл предложения. Следует читать так: Причина в том, что разрешение использования cmp3
в качестве аргумента ssort()
нарушило бы гарантию того, что cmp3()
вызовется с аргументами mytype*
.
Стр.296: 10.4.6.2. Члены-константы
Можно проинициализировать член, являющийся статической константой интегрального типа, добавив к объявлению члена константное выражение в качестве инициализирующего значения.
Вроде бы все хорошо, но почему только интегрального типа? В чем причина подобной дискриминации? Д-р Страуструп пишет по этому поводу следующее:
The reason for "discriminating against" floating points in constant expressions is that the precision of floating point traditionally varied radically between processors. In principle, constant expressions should be evaluated on the target processor if you are cross compiling.
Причина подобной "дискриминации" плавающей арифметики в константных выражениях в том, что обычно точность подобных операций на разных процессорах существенно отличается. В принципе, если вы осуществляете кросс-компиляцию, то такие константные выражения должны вычисляться на целевом процессоре.
Т.е. в процессе кросс-компиляции на процессоре другой архитектуры будет крайне проблематично абсолютно точно вычислить константное выражение, которое могло бы быть использовано в качестве литерала (а не адреса ячейки памяти) в машинных командах целевого процессора.
Судя по всему, за пределами задач кросс-компиляции (которые, к слову сказать, встречаются не так уж и часто) никаких проблем с определением нецелочисленных констант не возникает, т.к. некоторые компиляторы вполне допускают код вида
class Curious {
static const float c5=7.0;
};
в качестве (непереносимого) расширения языка.
Стр.297: 10.4.7. Массивы
Не существует способа явного указания аргументов конструктора (за исключением использования списка инициализации) при объявлении массива.
К счастью, это ограничение можно сравнительно легко обойти. Например, посредством введения локального класса:
#include <stdio.h>
struct A { // исходный класс
int a;
A(int a_) : a(a_) { printf("%d\n",a); }
};
void f()
{
static int vals[]={2, 0, 0, 4};
static int curr=0;
struct A_local : public A { // вспомогательный локальный
A_local() : A(vals[curr++]) { }
};
A_local arr[4];
// и далее используем как A arr[4];
}
int main()
{
f();
}
Т.к. локальные классы и их использование остались за рамками книги, далее приводится соответствующий раздел стандарта:
9.8 Объявления локальных классов [class.local]
Класс может быть определен внутри функции; такой класс называется локальным (local) классом. Имя локального класса является локальным в окружающем контексте (enclosing scope). Локальный класс находится в окружающем контексте и имеет тот же доступ к именам вне функции, что и у самой функции. Объявления в локальном классе могут использовать только имена типов, статические переменные, extern переменные и функции, перечисления из окружающего контекста. Например:
int x;
void f()
{
static int s;
int x;
extern int g();
struct local {
int g() { return x; } // ошибка, auto x
int h() { return s; } // OK
int k() { return ::x; } // OK
int l() { return g(); } // OK
};
// ...
}
local* p = 0; // ошибка: нет local в текущем контексте
Окружающая функция никаких специальных прав доступа к членам локального класса не имеет, она подчиняется обычным правилам (см. раздел 11 [class.access]). Функции-члены локального класса, если они вообще есть, должны быть определены внутри определения класса.
Вложенный классY
может быть объявлен внутри локального классаX
и определен внутри определения классаX
или же за его пределами, но в том же контексте (scope), что и классX
. Вложенный класс локального класса сам является локальным.