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

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


Оглавление по оригиналу
Введение
17 Задача 1.1. Итераторы
24 Задача 1.4. Строки, нечувствительные к регистру. Часть 2
27, 30 Задача 1.5. Обобщенные контейнеры с максимальным повторным использованием. Часть 2
36 Задача 1.6. Временные объекты
38, 41 Задача 1.8. Переключение потоков
43 Задача 1.9. Предикаты. Часть 1
45 Задача 1.10. Предикаты. Часть 2
51, 56, 58 Задача 1.11. Расширяемые шаблоны: путем наследования или свойств?
65 Задача 1.12. Typename
67, 71 Задача 1.13. Контейнеры, указатели и неконтейнеры
76 Задача 1.14. Использование vector и deque
79 Задача 1.15. Использование set и map
103 Задача 2.1. Разработка безопасного кода. Часть 1
112 Задача 2.3. Разработка безопасного кода. Часть 3
114 Задача 2.4. Разработка безопасного кода. Часть 4
120 Задача 2.5. Разработка безопасного кода. Часть 5
121, 122, 125 Задача 2.6. Разработка безопасного кода. Часть 6
128 Задача 2.8. Разработка безопасного кода. Часть 8
131 Задача 2.9. Разработка безопасного кода. Часть 9
138 Задача 2.12. Сложность кода. Часть 2
149 Задача 2.14. Исключения в конструкторах. Часть 2
151, 154 Задача 2.15. Неперехваченные исключения
164, 166, 168 Задача 2.18. Разработка безопасных классов. Часть 1
172 Задача 2.19. Разработка безопасных классов. Часть 2
176, 178, 179 Задача 3.1. Механика классов
186 Задача 3.3. Взаимоотношения классов. Часть 1
188, 192 Задача 3.4. Взаимоотношения классов. Часть 2
194 Задача 3.5. Наследование: потребление и злоупотребление
205, 207 Задача 3.8. Эмуляция множественного наследования
220 Задача 4.1. Минимизация зависимостей времени компиляции. Часть 1
222, 223, 224 Задача 4.2. Минимизация зависимостей времени компиляции. Часть 2
227, 228 Задача 4.4. Брандмауэры компиляции
233 Задача 4.5. Идиома "Fast Pimpl"
240, 241, 243, 245 Задача 5.2. Поиск имен и принцип интерфейса. Часть 2
259 Задача 6.1. Управление памятью. Часть 1
265, 272 Задача 6.3. Применение auto_ptr. Часть 1
283 Задача 6.6. Интеллектуальные указатели-члены. Часть 2
299 Задача 7.3. Отложенная оптимизация. Часть 2
309 Задача 7.5. Отложенная оптимизация. Часть 4
321 Задача 8.1. Рекурсивные объявления
324, 325 Задача 8.2. Имитация вложенных функций
344, 346 Задача 9.4. Времена жизни объектов. Часть 2
355 Задача 10.3. Корректность const
361 Задача 10.4. Приведения
Документация
Исходный код

Оглавление по оригиналу

17 E: Item 1. Iterators
24 E: Item 3. Case-Insensitive Strings-Part 2
30 E: Item 5. Maximally Reusable Generic Containers-Part 2. Alternative: The Standard Library Approach
36 E: Item 6. Temporary Objects
114 E: Item 11. Writing Exception-Safe Code-Part 4
122 E: Item 13. Writing Exception-Safe Code-Part 6
128 E: Item 15. Writing Exception-Safe Code-Part 8
131 E: Item 16. Writing Exception-Safe Code-Part 9
138 E: Item 19. Code Complexity-Part 2
176, 178, 179 E: Item 20. Class Mechanics
192 E: Item 23. Class Relationships-Part 2
194 E: Item 24. Uses and Abuses of Inheritance
220 E: Item 26. Minimizing Compile-time Dependencies-Part 1
223, 224 E: Item 27. Minimizing Compile-time Dependencies-Part 2
227 E: Item 29. Compilation Firewalls
233 E: Item 30. The "Fast Pimpl" Idiom
243 E: Item 32. Name Lookup and the Interface Principle-Part 2
259 E: Item 35. Memory Management-Part 1
265, 272 E: Item 37. AUTO_PTR
346 E: Item 41. Object Lifetimes-Part 2
355 E: Item 43. Const-Correctness
361 E: Item 44. Casts
38 M: Item 1. Switching Streams
43 M: Item 2. Predicates, Part 1: What remove() Removes
51, 56, 58 M: Item 4. Extensible Templates: Via Inheritance or Traits?
65 M: Item 5. Typename
67, 71 M: Item 6. Containers, Pointers, and Containers That Aren't
76 M: Item 7. Using vector and deque
79 M: Item 8. Using set and map
299 M: Item 14. Lazy Optimization, Part 2: Introducing Laziness
309 M: Item 16. Lazy Optimization, Part 4: Multithreaded Environments
149 M: Item 18. Constructor Failures, Part 2: Absorption?
154 M: Item 19. Uncaught Exceptions
164, 166, 168 M: Item 22. Exception-Safe Class Design, Part 1: Copy Assignment
172 M: Item 23. Exception-Safe Class Design, Part 2: Inheritance
205, 207 M: Item 25. Emulating Multiple Inheritance
283 M: Item 31. Smart Pointer Members, Part 2: Toward a ValuePtr
321 M: Item 32. Recursive Declarations
324 M: Item 33. Simulating Nested Functions

Введение

Перед вами находятся мои комментарии к переводу блестящих книг Herb Sutter "Excetional C++" и "More Excetional C++". Что можно сказать об оригинале? Прежде всего, хочу отметить, что Герб Саттер является одним из тех немногих авторов современных книг о C++, которые действительно понимают то, о чем пишут. Его мини-серия о создании exception safe кода является самой лучшей проработкой данного вопроса из всего, что до сих пор было издано на данную тему. Это своего рода классический материал, с которым должен ознакомиться каждый программист на C++. Действительно должен, т.к. при использовании современных средств C++ игнорировать исключения и связанные с ними вопросы уже просто невозможно. Помимо вопросов обработки исключений, в книге рассматривается множество других важных и, временами, не очень важных аспектов C++, свой взгляд на отдельные моменты которых я решил предоставить вашему вниманию.

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

Увы, но подавляющее большинство наших программистов все еще не может позволить себе купить оригиналы "Excetional C++" и "More Excetional C++", а те, кто не владеет английским, просто вынуждены "наслаждаться переводом". К счастью, (по подтвержденным слухам) в Рунете можно найти электронные варианты обоих книг на английском языке. Так что мои комментарии, первоначально ориентированные только на перевод, в настоящее время можно разделить на два класса:

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

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


Стр.17: Задача 1.1. Итераторы
E: Item 1. Iterators

В данной задаче нам предоставляется возможность найти ошибки в следующем коде:
int main()
{
 vector<Date> e;
 copy(istream_iterator<Date>(cin), istream_iterator<Date>(),
      back_inserter(e));
 vector<Date>::iterator first = find(e.begin(), e.end(), "01/01/95");
 vector<Date>::iterator last = find(e.begin(), e.end(), "12/31/95");
 *last = "12/30/95";
 copy(first, last, ostream_iterator<Date>(cout, "\n"));
 e.insert(--e.end(), TodaysDate());
 copy(first, last, ostream_iterator<Date>(cout, "\n"));
}
Думаю, что кроме указанных автором ошибок с итераторами стоит указать и на ошибки в "общечеловеческом смысле": Опасайтесь учебных примеров! Большинство из них построено по принципу: "Если A, то B", но то, что это самое A никогда не должно встречаться в реальном коде (в лучшем случае) подразумевается.

Стр.24: Задача 1.4. Строки, нечувствительные к регистру. Часть 2
E: Item 3. Case-Insensitive Strings-Part 2

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

Мои строки, вообще говоря, нулевые символы содержат, т.к. это не запрещено стандартом. И даже более того, некоторые мои приложения действительно вынуждены работать со строками, состоящими из любых символов, включая ноль ('\0'). Так что использование .c_str() для реализации "общих функций" типа operator+ недопустимо.

А сейчас несколько общих рассуждений. Прежде всего, необходимо четко себе представлять, что предлагаемый автором std::basic_string<char, ci_char_traits> класс не является строкой в том смысле, как это интуитивно подразумевает большинство пользователей. Дело в том, что с точки зрения языка std::basic_string<char, std::char_traits<char> > (т.е. std::string) и предложенный std::basic_string<char, ci_char_traits> являются двумя совершенно разными типами: вы не сможете использовать std::basic_string<char, ci_char_traits> там, где нужен std::string. И из этого тривиального наблюдения вырастает множество проблем.

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


Стр.27: Задача 1.5. Обобщенные контейнеры с максимальным повторным использованием. Часть 2

Поскольку конструктор по шаблону никогда не является...

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

Во-первых, вот перевод сноски 106 параграфа 12.8p2: Т.к. конструктор-шаблон никогда не является конструктором копирования, наличия такого шаблона не подавляет неявный конструктор копирования. Конструкторы-шаблоны участвуют в выборе перегруженного конструктора наравне с другими конструкторами, включая конструкторы копирования, и конструктор-шаблон может быть использован для копирования объекта, если он обеспечивает лучшее соответствие, чем остальные конструкторы.

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


Стр.30: Задача 1.5. Обобщенные контейнеры с максимальным повторным использованием. Часть 2
E: Item 5. Maximally Reusable Generic Containers-Part 2. Alternative: The Standard Library Approach

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

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

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

А теперь посмотрим на накладные расходы, возникающие там, где незнакомый с ассемблером программист их совершенно не ожидает. Рассмотрим следующий упрощенный пример:

30.cpp
struct A {
       int m[5];
//       int* m;

       int& operator[](int i) { return m[i]; }
};

int f(A& a, int i)
{
 return a[i];
}

int main()
{
}

Он нам нужен не для запуска, а для компиляции в ассемблер. Очевидно, что от изменения int m[5] на int* m исходный код operator[] не изменяется. Тем не менее, при этом изменяется машинный код, т.к. компилятор вынужден использовать дополнительный уровень косвенности: например, вместо

	mov edx,dword ptr [ebp+8]
	mov eax,dword ptr [ebp+12]
	mov eax,dword ptr [edx+4*eax]
может получиться
	mov eax,dword ptr [ebp+8]
	mov edx,dword ptr [eax]
	mov eax,dword ptr [ebp+12]
	mov eax,dword ptr [edx+4*eax]
Дело в том, что в случае int m[5] массив хранится прямо в объекте, а для int* m объект хранит только указатель на массив, так что для получения адреса массива приходится дополнительно извлекать из объекта значение указателя. А дополнительное разыменование указателя для тривиального operator[] может привести к существенной потере производительности, т.к. индексация, как правило, используется в цикле.

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

30a.cpp
#include <stdlib.h>
#include <time.h>
#include <stdio.h>

const int nelem=1000;  // количество элементов в векторе

class Vec1 {
      int m[nelem];
 public:
      int& operator[](int i) { return m[i]; }
};

class Vec2 {
      int* m;
 public:
      Vec2() : m(new int[nelem]) {}
      ~Vec2() { delete [] m; }
      int& operator[](int i) { return m[i]; }
};

template<class T>
void f(long niters)
{
 T v;
 clock_t c1=clock();

 for (long i=0; i<niters; i++)
     for (long j=1; j<1000; j++)
         for (long k=1; k<nelem; k++) v[k-1]=v[k];

 clock_t c2=clock();
 printf("push: %ld thnds passes per %.1f sec\n", niters, double(c2-c1)/CLK_TCK);
}

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

 puts("Vec1");
 f<Vec1>(niters);

 puts("Vec2");
 f<Vec2>(niters);
}

Данный код запускался мной на двух реализациях, замедление составило от 26 до 70%.


Стр.36: Задача 1.6. Временные объекты
E: Item 6. Temporary Objects

 list<Employee>::const_iterator end(emps.end());
 for (list<Employee>::const_iterator i = emps.begin(); i != end; ++i)
Вообще-то, классический цикл перебора всех элементов пишут так:
 for (list<Employee>::const_iterator i=emps.begin(), end=emps.end(); i!=end; ++i)
Кроме того, еще раз повторяю: никогда не используйте std::find для поиска элементов путем последовательного перебора, если только вы не абсолютно уверены, что количество элементов, которое придется при этом просмотреть, невелико (скажем, меньше 20). Для приемлемого по скорости поиска нужно использовать специально для этого предназначенные структуры данных и/или алгоритмы.

Стр.38: Задача 1.8. Переключение потоков
M: Item 1. Switching Streams

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

Скажу сразу, что на платформах, где флаг ios::binary имеет смысл (например Windows) это невозможно, т.к. стандартные потоки ввода/вывода cin и cout открываются startup кодом в текстовом режиме, так что при вызове echo <infile >outfile выходной файл, вообще говоря, не будет совпадать со входным.

К тому же, используемый автором флаг ios::binary для варианта echo infile outfile приведет к тому, что результат работы будет отличаться от echo <infile >outfile.

Т.о. правильная формулировка задания должна звучать так: "... повторяет вводимую текстовую информацию и одинаково работает ...", а код не должен использовать ios::binary.


Стр.41: Задача 1.8. Переключение потоков

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

Ну что же, еще раз скажем переводчикам спасибо за наше счастливое детство...

Дорогие переводчики! Слово single в данном контексте означает единственной, а не единой. Не стоит останавливать поиск в словаре на первом попавшемся значении.

Итак, автор имел ввиду, что каждая часть кода должна отвечать за свою, одну-единственную задачу (и делать это хорошо).

Далее. В оригинале на этом месте стоит рекомендация: "Prefer encapsulation. Separate concerns", т.е. "Предпочитайте инкапсуляцию. Разделяйте ответственность". Каким образом она превратилась в приведенную выше "Каждая часть кода..." рекомендацию из "E: Item 12. Writing Exception-Safe Code-Part 5" остается загадкой. Вероятно, некстати подвернулась неудачная фаза Луны.


Стр.43: Задача 1.9. Предикаты. Часть 1
M: Item 2. Predicates, Part 1: What remove() Removes

Напишите код, удаляющий все значения, равные трем, из std::vector<int>.

К сожалению, автор опять ничего не сказал о том, что удаление произвольных элементов из std::vector -- это не очень удачная идея. Почти такая же плохая, как и использование функции std::remove.


Стр.45: Задача 1.10. Предикаты. Часть 2

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

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

Не устаем благодарить дорогую редакцию за дословный перевод. Без всякой претензии на понимание смысла.


Стр.51: Задача 1.11. Расширяемые шаблоны: путем наследования или свойств?
M: Item 4. Extensible Templates: Via Inheritance or Traits?

... поскольку попытка вызова T::Clone() без параметров будет успешной и в том случае, когда Clone() имеет параметры по умолчанию ...

Вспоминая известный анекдот так и хочется спросить: "Вам шашечки или ехать?" Ох уж мне эти учебные примеры!

Дело в том, что задача в такой постановке лишена практического смысла, т.к. в реальном мире мы просто напишем ptr=t.Clone(), а то, что у Clone() могут быть параметры по умолчанию нас нисколько не должно волновать, коль скоро вызов компилируется и делает то, что нам нужно.

К тому же, рядом с чрезмерной строгостью к наличию/отсутствию параметров по умолчанию находится неоправданное легкомыслие по отношению к правам доступа к T::Clone(). Согласитесь, что ситуация, когда столь желанная T* T::Clone() существует, но недоступна не многим лучше, чем просто ее отсутствие.

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


Стр.56: Задача 1.11. Расширяемые шаблоны: путем наследования или свойств?
M: Item 4. Extensible Templates: Via Inheritance or Traits?

... если D* может быть преобразовано в B*, то будет выбрана функция Test(B*) ... в противном случае будет выбрана функция Test(...) ...

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

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

struct B {};
struct D : private B {};

int i=IsDerivedFrom1<D, B>::Is;
Определяя значение IsDerivedFrom1<D, B>::Is компилятор сначала выбирает вызов Test(B*) как более подходящий согласно правилам перегрузки, а затем смотрит на права доступа: т.к. B является private базовым классом класса D, а класс IsDerivedFrom1<D, B> не является его другом, то налицо попытка нарушить права доступа. Получаем ошибку компиляции.

Стр.58: Задача 1.11. Расширяемые шаблоны: путем наследования или свойств?
M: Item 4. Extensible Templates: Via Inheritance or Traits?

class IsDerivedFrom2
{
      static void Constraints(D* p)
      {
       B* pb = p;
       pb = p;
      }
И здесь мы видим все ту же проблему: если класс D использует закрытое (или защищенное) наследование и не объявит IsDerivedFrom2<D, B>::Constraints() другом, то в момент использования наследования от IsDerivedFrom2<D, B> нас будет ждать все та же ошибка компиляции.

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

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

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

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


Стр.65: Задача 1.12. Typename
M: Item 5. Typename

В качестве маленькой премии -- код-шутка с использованием typename.

Как не трудно видеть, результатом работы приведенного автором кода будет Прелесть!Прелесть! "А где же шутка"? -- спросите вы? Шутка в том, что в английском языке существует пословица о том, что "роза всегда хорошо пахнет"... [истерический хохот в зале]


Стр.67: Задача 1.13. Контейнеры, указатели и неконтейнеры
M: Item 6. Containers, Pointers, and Containers That Aren't

Очевидное решение состоит в построении второй структуры, вероятно map<PhoneNumber*, Name*, Deref>, которая обеспечивает возможность обратного поиска и при этом избегает дублирования хранимой информации.

М-да... Пожалуй, начнем с того, что "очевидным решением" является разработка абстрактного базового класса (interface в Java-терминологии), который инкапсулирует всю нетривиальную машинерию, четко разграничивая код пользователя (с его нуждами) и реализацию, которая со временем будет изменяться.

Ну а продолжим мы тем, что поиск имени по указателю на (некоторый объект) PhoneNumber лишен всякого практического смысла:

void f(PhoneNumber& pn, map<PhoneNumber*, Name*, Deref>& m)
{
 m.find(&pn);   // возможно и сработает
 PhoneNumber pn2(pn);
 m.find(&pn2);  // а здесь уже точно приплыли
}
Короче, еще один "учебный пример"...

Стр.71: Задача 1.13. Контейнеры, указатели и неконтейнеры
M: Item 6. Containers, Pointers, and Containers That Aren't

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

Признаюсь честно: такие "правила" на меня действуют как красная тряпка на быка. Что значит никогда не оптимизируй?

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

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

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


Стр.76: Задача 1.14. Использование vector и deque
M: Item 7. Using vector and deque

... у deque нет функций-членов capacity() и reserve(), которые есть у вектора, но это нельзя считать большой потерей; на самом деле эти функции -- недостаток vector ...

Как сказал бы один наш преподаватель: "Ну вы, Герб, крепко рубанули!" Ничего себе недостаток -- возможность заранее указать размер непрерывной области памяти, скоро понадобящейся вектору (надеюсь, вы понимаете, что std::vector преднамеренно использует непрерывный блок памяти для ускорения доступа по индексу).

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


Стр.79: Задача 1.15. Использование set и map
M: Item 8. Using set and map

Итак, вот мы и добрались до ассоциативных контейнеров. Прежде всего, сделаю маленькое замечание по приведенному автором коду: при вставке элементов в map вместо вспомогательной функции make_pair(), требующей тщательного приведения типа аргументов, следует использовать тип value_type, который всегда публикуется стандартными контейнерами для нашего удобства:
m.insert(map_is::value_type(9999999,s));
где map_is -- это typedef для map<int, string>, определенный нами где-то выше.

Ну а теперь приступим к главному: интерфейс ассоциативных контейнеров в STL определен крайне неудачно -- никаких пар pair<const Key, Value> в качестве содержащихся в контейнере значений (тип value_type) там и близко не должно было быть! Тип хранящихся в карте значений -- это просто Value, а где и как хранить Key -- личное дело самого контейнера.

Дабы проиллюстрировать естественность данного подхода, давайте посмотрим на vector<string> -- концептуальный аналог карты map<int, string> (надеюсь, вы не забыли, что карты часто называют ассоциативными массивами?):

void f(map<int, string>& m, vector<string>& v)
{
 string s=m[5];
 m[7]="что-то умное";

 s=v[5];
 v[7]="а здесь просто шутка";
}
Как видите, и карта и массив используются одинаково (фактически, карты специально так и создавались). Но, в отличие от карты, при использовании массива вопрос о том как бы это не испортить значение ключа в уже занесенной паре (ключ, значение) нам просто не приходит в голову -- с существующим интерфейсом массива сделать это просто невозможно.

В частности, обратите внимание на интерфейс класса hash_vec, идущий вразрез с общепринятой идеологией итераторов, но допускающий более эффективную реализацию.


Стр.103: Задача 2.1. Разработка безопасного кода. Часть 1

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

Здесь имеется ввиду, что контейнеры будут одновременно и exception-safe и exception-neutral. Хоть из exception-neutral и следует exception-safe, но обратное, вообще говоря, неверно, т.к. вместо перевозбуждения оригинального исключения можно возбудить другое (например, содержащее сообщение о текущем контексте ошибки плюс информацию об оригинальном исключении).


Стр.112: Задача 2.3. Разработка безопасного кода. Часть 3

... безопасность исключений влияет на разработку класса.

Браво, господа переводчики! Браво!

Это же как надо постараться, чтобы из простого "exception safety affects your class's design" соорудить такую тавтологию?!


Стр.114: Задача 2.4. Разработка безопасного кода. Часть 4
E: Item 11. Writing Exception-Safe Code-Part 4

Прежде всего, приведу названия гарантий на языке оригинала -- basic, strong, nothrow. А теперь, остановимся на сути первых двух, т.к. уяснение смысла третьей будет оставлено читателю в качестве упражнения.
  1. Basic guarantee. По сути, это требование к отсутствию ошибок, т.е. код, не удовлетворяющий первой гарантии, является некорректным.

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

    void f(B&);
    
    try {
        f(b);
    }
    catch (...) {
          // считаем, что содержимое объекта b изменилось
    }
  2. Strong guarantee. По сути, это некоторый аналог транзакций: в случае вонзикновения исключений все произведенные функцией изменения будут откачены (rollback).

    Самое сложное в этой гарантии -- это понимание того, что если обеспечение basic guarantee не привело автоматически к strong guarantee, то и не нужно пытаться ее обеспечить. Дело в том, что обеспечение strong guarantee "вручную" производится путем введения дополнительных копий объектов, что чаще всего приводит к ненужным накладным расходам, т.к. пользователь, как правило, и не рассчитывал воспользоваться "строгостью" гарантии, а просто позволит процессу раскрутки стека уничтожить все изменявшиеся объекты и запустит другую операцию, отметив где-то, что предыдущая не удалась.

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

    void f(B&);
    
    B b2(b);  // делаем копию
    try {
        f(b);
    }
    catch (...) {
          // содержимое b изменилось, но у нас есть копия b2
    }

Стр.120: Задача 2.5. Разработка безопасного кода. Часть 5

  1. Зачем нужен класс StackImpl?

    Если кратко и по сути, то собственно вспомогательный класс, а не набор вспомогательных функций, нам нужен из-за его деструктора. Вызовы деструктора будут автоматически вставляться компилятором, освобождая нас от обременительных блоков try/catch. Да, это техника RAII (resource acquisition is initialization).

  2. Чем же должен быть заменен комментарий /*????*/...

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


Стр.121: Задача 2.6. Разработка безопасного кода. Часть 6

Всегда используйте идиому "захвата ресурса при инициализации"...

Здесь имеется ввиду техника RAII (resource acquisition is initialization), обычно переводимая как "выделение ресурса есть инициализация". А захватывают ресурсы только лживые ястребы империализма.


Стр.122: Задача 2.6. Разработка безопасного кода. Часть 6
E: Item 13. Writing Exception-Safe Code-Part 6

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

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

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

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

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

122.cpp
#include <algorithm>
#include <string>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>

using namespace std;

const int nelem=100;  // количество элементов в стеке

template <class T1, class T2>
inline void construct(T1* p, const T2& value) { new(p) T1(value); }

template <class T>
inline void destroy(T* p) { p->~T(); }

template <class FwdIter>
void destroy(FwdIter first, FwdIter last)
{
 for ( ; first!=last; ++first) destroy(&*first);
}

template <class T>
class StackImpl {
 protected:
      StackImpl(size_t size=0) :
        v_(static_cast<T*>(size ? operator new(sizeof(T)*size) : 0)),
        vsize_(size), vused_(0) {}

      ~StackImpl()
      {
       destroy(v_, v_+vused_);
       operator delete(v_);
      }

      T* v_;
      size_t vsize_;
      size_t vused_;
};

// оригинальный стек
template <class T>
class Stack : private StackImpl<T> {
 public:
      Stack(size_t size=0) : StackImpl<T>(size) {}
      Stack(const Stack&);
      Stack& operator=(const Stack&);
      void Swap(Stack&);
      size_t Count() const { return vused_; }
      void Push(const T&);
};

template <class T>
Stack<T>::Stack(const Stack& other) : StackImpl<T>(other.vused_)
{
 for ( ; vused_<other.vused_; ++vused_) construct(v_+vused_, other.v_[vused_]);
}

template <class T>
Stack<T>& Stack<T>::operator=(const Stack& other)
{
 Stack temp(other);
 Swap(temp);
 return *this;
}

template <class T>
void Stack<T>::Swap(Stack& other)
{
 swap(v_, other.v_);
 swap(vsize_, other.vsize_);
 swap(vused_, other.vused_);
}

template <class T>
void Stack<T>::Push(const T& t)
{
 if (vused_==vsize_) {
    Stack temp(vsize_*2+1);
    while (temp.Count()<vused_) temp.Push(v_[temp.Count()]);
    temp.Push(t);
    Swap(temp);
 }
 else {
      construct(v_+vused_, t);
      ++vused_;
 }
}

// измененная версия
template <class T>
class Stack2 : private StackImpl<T> {
 public:
      Stack2(size_t size=0) : StackImpl<T>(size) {}
      Stack2(const Stack2&);
      Stack2& operator=(const Stack2&);
      void Push(const T&);
};

template <class T>
Stack2<T>::Stack2(const Stack2& other) : StackImpl<T>(other.vused_)
{
 for ( ; vused_<other.vused_; ++vused_) construct(v_+vused_, other.v_[vused_]);
}

template <class T>
Stack2<T>& Stack2<T>::operator=(const Stack2& other)
{
 if (vsize_<other.vused_) {  // места меньше чем элементов в other
    destroy(v_, v_+vused_);
    vused_=0;

    operator delete(v_);
    v_=0;
    vsize_=0;

    v_=(T*)operator new(sizeof(T)*other.vused_);
    vsize_=other.vused_;
 }

 if (vused_<=other.vused_) {  // other длиннее
    for (int i=0; i<vused_; i++)  // присваиваем то, что можно присвоить
        v_[i]=other.v_[i];

    for ( ; vused_<other.vused_; vused_++)  // создаем оставшиеся
        construct(v_+vused_, other.v_[vused_]);
 }
 else {  // other короче
      for (int i=0; i<other.vused_; i++)  // присваиваем то, что можно присвоить
          v_[i]=other.v_[i];

      for ( ; vused_>other.vused_; vused_--)  // уничтожаем лишние
          destroy(v_+vused_-1);
 }
 
 return *this;
}

template <class T>
void Stack2<T>::Push(const T& t)
{
 if (vused_==vsize_) {
    size_t nvsize=vsize_*2+1;
    T* nv=(T*)operator new(sizeof(T)*nvsize);
    int i=0;

    try {
        for ( ; i<vused_; i++) construct(nv+i, v_[i]);
        construct(nv+vused_, t);
    }
    catch (...) {
          destroy(nv, nv+i);
          operator delete(nv);
          throw;
    }

    destroy(v_, v_+vused_);
    operator delete(v_);

    v_=nv;
    vsize_=nvsize;
 }
 else construct(v_+vused_, t);

 vused_++;
}

template <template <class> class S, class T>
void assign(const T& t, long niters)
{
 S<T> s1, s2;
 for (int i=0; i<nelem; i++) {
     s1.Push(t);
     if (i<nelem/3) s2.Push(t);
 }

 clock_t c1=clock();

 for (long i=0; i<niters; i++)
     for (long j=0; j<1000; j++) {
         S<T> s;
         s=s1;
         s1=s2;
         s2=s;
     }

 clock_t c2=clock();
 printf("assign: %ld thnds calls per %.1f sec\n", niters,
   double(c2-c1)/CLK_TCK);
}

template <template <class> class S, class T>
void push(const T& t, long niters)
{
 clock_t c1=clock();

 for (long i=0; i<niters; i++)
     for (long j=0; j<1000; j++) {
         S<T> s;
         for (int i=0; i<nelem; i++) s.Push(t);
     }

 clock_t c2=clock();
 printf("push: %ld thnds calls per %.1f sec\n", niters, double(c2-c1)/CLK_TCK);
}

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

 puts("string");
 string s("просто строка");
 assign<Stack>(s, niters);
 assign<Stack2>(s, niters);
 push<Stack>(s, niters);
 push<Stack2>(s, niters);

 puts("int");
 int i=5;
 assign<Stack>(i, niters);
 assign<Stack2>(i, niters);
 push<Stack>(i, niters);
 push<Stack2>(i, niters);
}

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

Stack<string> Stack<int>
assign push assign push
ориг. изм. уск. ориг. изм. уск. ориг. изм. уск. ориг. изм. уск.
Реализация 1 7.4 9.6 -30% 8.9 7.9 11% 1.6 0.7 56% 3.2 2.3 28%
Реализация 2 4.9 3.7 24% 7.0 6.4 9% 0.8 0.6 25% 2.5 1.9 24%

Анализ:

  1. Несмотря на то, что изменения вносились для увеличения производительности, в одном из случаев (оператор присваивания Stack<string>, первая реализация) получено замедление работы на 30%. Вот вам и оптимизация!
  2. Ни в одном из наблюдавшихся вариантов существенного прироста производительности получить не удалось. Максимальное ускорение -- 56%. Данный результат особенно знаменателен тем, что в реальном коде операции со стеком окажутся разбавленными остальной логикой работы программы, так что реальное изменение производительности окажется еще меньше.
  3. Наблюдаемая картина существенно зависит от реализации.
Выводы:
  1. Оптимизация -- штука коварная. И если только вы не создаете библиотечный код промышленного уровня не стоит гнаться за каждой мелочью. Оптимизированный код зачастую на порядки сложнее, а получаемый от этого выигрыш может оказаться не настолько заметным. Тем более, что для некоторых типов элементов оптимизация обобщенного контейнера может обернуться "пессимизацией".
  2. Отказ от обеспечения строгой гарантии (там, где она не получается автоматически) приводит, как правило, к повышению эффективности кода.
  3. "Стандартный" оператор присваивания
    T& operator=(const T& t)
    {
     T tmp(t);
     swap(tmp);
     return *this;
    }
    как правило, не приводит к существенным накладным расходам. А его простота и безопасность делают его крайне привлекательным для практически повсеместного использования.

Стр.125: Задача 2.6. Разработка безопасного кода. Часть 6

Кто там говорил о том, что написать безопасный код трудно?

В оригинале: ... we've written a fully exception-safe and exception-neutral generic container without writing a single try! (Who says writing exception-safe code is trying?)


Стр.128: Задача 2.8. Разработка безопасного кода. Часть 8
E: Item 15. Writing Exception-Safe Code-Part 8

  1. Кроме того, не требуется копирующее присваивание...

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

    Не удивлюсь, если окажется, что иногда все-таки стоит. Как правило, добавление еще одного параметра шаблона (с удачно выбранным) значением по умолчанию является прекрасным решением подобного рода проблем: template <class T, class UseAssign=Yes> class Stack. Если использование оператора присваивания нежелательно, то пользователь сможет его отменить: Stack<string, No>.

  2. Следует ли использовать спецификации исключений для функций-членов Stack?

    Спецификации исключений в том виде, как это определено в стандарте не только бесполезны, но и вредны. Более подробно об этом можно почитать в статье автора A Pragmatic Look at Exception Specifications.


Стр.131: Задача 2.9. Разработка безопасного кода. Часть 9
E: Item 16. Writing Exception-Safe Code-Part 9

... он не может корректно обработать ситуацию генерации нескольких исключений ...

Вопрос в том, что именно считать "корректной обработкой нескольких исключений"? Из того, что мы имеем на сегодняшний день, можно сделать вывод о том, что, грубо говоря, в C++ мы всегда можем возбудить исключение; после чего начинается раскрутка стека, в процессе которой языком вызываются деструкторы подлежащих уничтожению объектов. Так вот, эти самые деструкторы не имеют право выбрасывать исключения за свои пределы, т.к. иначе получится "пересечение" двух исключений и, как следствие, завершение работы посредством вызова std::terminate().

Т.о. мне представляется корректным такой дизайн функции destroy(), при котором:

  1. Возбуждение исключения одним из деструкторов уничтожаемых элементов не останавливает уничтожение остальных. После успешного уничтожения всех оставшихся элементов возбужденное исключение будет передано вызвавшему коду.
  2. Повторное возбуждение исключения должно рассматриваться как исключение, покинувшее деструктор в процессе раскрутки стека. Со всеми подобающими последствиями.
В результате чего, мы приходим к следующей реализации:
template <class FwdIter>
void destroy(FwdIter first, FwdIter last)
{
 class Guard {
       FwdIter& beg;
       FwdIter end;
  public:
       Guard(FwdIter& first, FwdIter last) : beg(first), end(last) {}
       ~Guard()
       {
        if (beg!=end) {
           ++beg;  // пропускаем вызвавший исключение элемент
           for ( ; beg!=end; ++beg) destroy(&*beg);
        }
       }
 } guard(first, last);

 for ( ; first!=last; ++first) destroy(&*first);
}
По сути, она отличается тем, что для удаления оставшихся элементов используется деструктор локального класса Guard. Т.о. если повторное исключение и возникнет, то это произойдет в деструкторе объекта, вызванного во время раскрутки стека. К сожалению, то, что объект запоминает ссылку на локальную переменную приводит к ухудшению генерируемого кода на большинстве реализаций, т.е. за повышенную безопасность приходится платить.

Стр.138: Задача 2.12. Сложность кода. Часть 2
E: Item 19. Code Complexity-Part 2

... функция EvaluateSalaryAndReturnName() имеет два побочных действия.

Здесь автором допущена очевидная смысловая ошибка: возврат строки побочным действием (side effect) не является. Крайне неудачный учебный пример при иллюстрации достаточно серьезных понятий.


Стр.149: Задача 2.14. Исключения в конструкторах. Часть 2
M: Item 18. Constructor Failures, Part 2: Absorption?

... но это будет ложь, поскольку обеспечить отсутствие исключений вы не в состоянии.

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

А теперь маленькое философское отступление. Те, кто писал действительно серьезный код знают, что исключений, возбуждаемых при ошибках программиста (напр. std::logic_error, который определяется стандартом как: для извещения об ошибках, которые, предположительно, могут быть обнаружены до выполнения программы) быть не должно. Самое лучшее, что мы можем сделать при обнаружении подобного рода ошибки -- это сразу же прервать выполнение программы, сгенерировав информацию, необходимую для определения сути происшедшего (напр. core dump). Дело в том, что "ошибка здесь" является, как правило, следствием "ошибки там" и продолжение исполнения некорректного кода в надежде на лучшее приведет к Ужасным Последствиям (напр. сумма, эквивалентная нескольким десяткам лет зарплаты программиста, будет зачислена на неизвестный счет где-нибудь в Буркина-Фасо. Но это, конечно, маловероятно, т.к. чаще всего обнаруженное несоответствие двойной бухгалтерии приводит к временной остановке всех операций до выяснения причин; а убыток от нескольких часов простоя серьезного банка приводит к гораздо более дорогостоящим последствиям...).


Стр.151: Задача 2.15. Неперехваченные исключения

т.е. в процессе свертки стека

Браво, господа-переводчики, браво! Это же сколько э... о! "технической смелости" и "непробиваемого природного оптимизма" надобно иметь, чтобы вполне устоявшийся термин раскрутка стека (stack unwinding) перевести как свертка стека?!


Стр.154: Задача 2.15. Неперехваченные исключения
M: Item 19. Uncaught Exceptions

Мой совет: не используйте ее.

Особую пикантность этому, безусловно правильному, совету придает использование std::uncaught_exception() в стандарте! Цитирую по 27.6.2.3p4: If ((os.flags() & ios_base::unitbuf) && !uncaught_exception()) is true, calls os.flush().

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


Стр.164: Задача 2.18. Разработка безопасных классов. Часть 1
M: Item 22. Exception-Safe Class Design, Part 1: Copy Assignment

... не имеется ни способа атомарного изменения обоих членов класса t1_ и t2_, ни способа надежного отката в согласованное состояние ...

К счастью, в большинстве случаев выход есть, и мы его уже много раз видели:

Widget& Widget::operator=(const Widget& w)
{
 T1 t1(w.t1_);
 T2 t2(w.t2_);

 Swap(t1_, t1);
 Swap(t2_, t2);

 return *this;
}
конечно, при условии, что существуют соответствующие функции Swap(), но, судя по всему, данное условие никак нельзя назвать чрезмерно обременительным.

Стр.166: Задача 2.18. Разработка безопасных классов. Часть 1
M: Item 22. Exception-Safe Class Design, Part 1: Copy Assignment

... если вы хотите скрыть WidgetImpl, вы должны написать свой собственный деструктор Widget, даже если это тривиальный деструктор.

Здесь имеется ввиду, что в точке определения деструктора Widget класс WidgetImpl должен быть объявленным полностью. Дело в том, что деструктор Widget является точкой инстанциирования деструктора класса auto_ptr<WidgetImpl>, который будет пытаться удалить указатель на WidgetImpl, а для корректного удаления класс WidgetImpl должен быть полностью определен.

Рассмотрим следующий пример. В нем класс A реализуется посредством A::B:

a.hpp
#ifndef __A_HPP__
 #define __A_HPP__

#include "sh_ptr.hpp"

struct A {
       struct B;
       sh_ptr<B> impl;

       A();
       ~A();
};

#endif

a.cpp
#include <stdio.h>
#include "a.hpp"

struct A::B {
       B() { puts("B()"); }
       ~B() { puts("~B()"); }
};

A::A() : impl(new B)
{
 puts("A()");
}

A::~A()
{
 puts("~A()");
}

Отметим, что деструктор ~A() объявлен в a.hpp, а реализован в a.сpp. Но некоторые реализации отказываются компилировать даже этот (корректный) вариант, ссылаясь на то, что класс A::B не определен в пределах a.hpp.

166.cpp
#include "a.hpp"

int main()
{
 A a;
}

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

B()
A()
~A()
~B()
Если же мы определим деструктор ~A() прямо в классе или же вообще его удалим (что эквивалентно определению в классе A пустого деструктора), то или же компилятор выдаст ошибку компиляции (объявление класса A::B недоступно в точке использования ~A()), или же код будет скомпилирован, но на этапе исполнения нас постигнет undefined behavior, что в нашем случае чаще всего будет означать не вызванный деструктор класса A::B.

Стр.168: Задача 2.18. Разработка безопасных классов. Часть 1
M: Item 22. Exception-Safe Class Design, Part 1: Copy Assignment

Скотт Мейерс (Scott Meyers) пишет следующее.

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


Стр.172: Задача 2.19. Разработка безопасных классов. Часть 2
M: Item 23. Exception-Safe Class Design, Part 2: Inheritance

... то невозможно написать строго безопасный оператор T::operator=(), конечно если U не предоставляет соответствующей возможности посредством некоторой другой функции (но если такая функция имеется, то почему это не U::operator=()?).

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

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

class T : private U, private V {  // два базовых класса
      // ...
};

T& T::operator=(const T& other)
{
 U::operator=(other);
 V::operator=(other);
 return *this;
}
Несмотря на то, что и U::operator=() и V::operator=() обеспечивают строгую гарантию, их последовательное применении строгой гарантии уже не дает. В самом деле: если V::operator=() возбудит исключение, то подобъект T::V не пострадает, в то время как T::U уже безвозвратно изменит свое значение (кстати, аналогичная картина будет наблюдаться и в том случае, когда V будет членом T, а не базовым классом).

К счастью, решение есть. И называется оно two phase commit:

T& T::operator=(const T& other)
{
 // первая фаза
 U u(other);
 V v(other);

 // вторая
 U::Swap(u);
 V::Swap(v);

 return *this;
}
На первой фазе присваивания мы пробуем выполнить (в сторонке) всю работу, которая может не пройти. А на второй -- подтверждаем произведенные изменения защищенным от сбоев способом.

Стр.176: Задача 3.1. Механика классов
E: Item 20. Class Mechanics

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

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


Стр.178: Задача 3.1. Механика классов
E: Item 20. Class Mechanics

  1. Кроме того, если вы хотите обеспечить пользователям удобство сложения объектов Complex с величинами типа double, имеет смысл предоставить также перегруженные функции operator+(const Complex&, double) и operator+(double, const Complex&).

    Как вы уже, видимо, догадались, удобство здесь не при чем, т.к. вызов x+2 будет воспринят как operator+(x, Complex(2)). А определение дополнительных операторов, принимающих аргументы типа double, имеет смысл для повышения производительности.

  2. добавьте виртуальную функцию-член и реализуйте ее посредством соответствующей функции-нечлена

    Наши любимые переводчики в который раз оторвались. Автором имелось ввиду следующее: создайте (закрытую) виртуальную функцию и вызывайте ее из функции-нечлена. Например:

    class Base {
     private:
          virtual Stream& vprint(Stream&);  // да, производные классы могут замещать
                                            // даже private виртуальные функции
          // ...
    };
    
    inline Stream& operator<<(Stream& s, const Base& b)
    {
     return b.vprint(s);
    }

Стр.179: Задача 3.1. Механика классов
E: Item 20. Class Mechanics

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

Здесь автором имеется ввиду, что переопределяемый оператор << должен правильно реагировать на установленные пользователем флаги формата. Например, значение ширины поля сбрасывается в ноль после каждого вывода в поток, так что наивная реализация применит устаноленное значение к "(", а не ко всему "(1,2)".


Стр.186: Задача 3.3. Взаимоотношения классов. Часть 1

  1. Никогда не используйте открытое наследование, за исключением модели ЯВЛЯЕТСЯ и РАБОТАЕТ КАК. Все замещающие функции-члены при этом не должны предъявлять повышенные требования к условиям своей работы и не должны иметь пониженную функциональность по сравнению с оригиналом.

    А хотите знать, что было написано в оригинале? Извольте-с:

    Never use public inheritance except to model true Liskov IS-A and WORKS-LIKE-A. All overridden member functions must require no more and promise no less.

    Вот так, стараниями дорогих переводчиков простые и понятные высказывания превращаются в словесный понос. Тоска...

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

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

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

    Еще один понос. В оригинале:

    Never inherit publicly to reuse code (in the base class); inherit publicly in order to be reused (by code that uses base objects polymorphically).

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


Стр.188: Задача 3.4. Взаимоотношения классов. Часть 2

Как говорится, нет предела совершенству! А по сему, в процессе погони за длинным центом наши доблестные переводчики просто выкинули не понравившийся им кусок книги (хотя, конечно, может статься и так, что в процессе честной и кропотливой работы компетентных акул пера и словаря он просто закатился под стол). Итак, после определения класса GenericTableAlgorithm нужно добавить:

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

class MyAlgorithm : public GenericTableAlgorithm
{
      // замещаем Filter() и ProcessRow() для реализации специфических действий
};

int main()
{
 MyAlgorithm a("Customer");
 a.Process();
}

Стр.192: Задача 3.4. Взаимоотношения классов. Часть 2
E: Item 23. Class Relationships-Part 2

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

Хоть и положение запятой, как нам известно с детства, может играть весьма существенную роль, вариант использования шаблона абсолютно отпадает по той простой причине, что его использование заставит нас вместо прототипа GenericTableAlgorithm(const string& table, GTAClient& method); включить в заголовочный файл реализацию данной функции, которая в реальной жизни потянет за собой еще черт знает что.

Да, в стандарте определен мертворожденный export, который вроде бы и должен был решать подобного рода проблемы. Но на самом деле, как и абсолютное большинство того, что создается (а не узаконивается) комитетами по стандартизации, export: во-первых, некорректно определен, а во-вторых -- не решает тех задач, чьей проблематикой он обязан своему существованию. Более подробно об этом можно почитать в соответствующих материалах автора Why We Can't Afford Export.


Стр.194: Задача 3.5. Наследование: потребление и злоупотребление
E: Item 24. Uses and Abuses of Inheritance

... нет ничего такого, что мы могли бы сделать с одиночным членом MyList<T> и не могли бы при наследовании от MyList<T>.

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

class D {
      B b;  // включаем подобъект B

      void f(const B& b2)
      {
       b.~B();         // уничтожаем старый подобъект
       new(&b) B(b2);  // и создаем новый, копируя b2
      }
};
Стандартом гарантируется, что данный код будет корретно работать: был подобъект b типа B -- он же и остался.

А вот в случае наследования

class D : B {  // наследуем подобъект B
      void f(const B& b2)
      {
       B* ptr=this;     // получаем указатель на подобъект B
       ptr->~B();
       new(ptr) B(b2);  // копируем b2, уничтожая служебную информацию
      }
};
подобъект B имел тип "B -- часть D" (напр. его таблица виртуальных функций указывала на функции класса D), а после копирования -- уже "просто B" (его таблица виртуальных функций уже указывает на собственные функции). Для наглядного знакомства со следствиями можно использовать следующий код:

194.cpp
#include <stdio.h>
#include <new>

struct B {
       int i;
       B(int i_) : i(i_) {}
       virtual void vf() { printf("B::vf %d\n", i); }
};

struct D : B {
       D(int i) : B(i) {}
       virtual void vf() { printf("D::vf %d\n", i); }

       void f(const B& b2)
       {
        B* ptr=this;
        ptr->~B();
        new(ptr) B(b2);
       }
};

int main()
{
 D d(1);
 B* ptr=&d;
 ptr->vf();  // выводит D::vf 1

 d.f(B(2));
 ptr->vf();  // выводит B::vf 2
}

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


Стр.205: Задача 3.8. Эмуляция множественного наследования
M: Item 25. Emulating Multiple Inheritance

void g1(A& x) { cout<<"g1:"<<x.Name()<<endl; }

Естественно, все семейство функций g() должно принимать аргументы по значению, а не по ссылке.


Стр.207: Задача 3.8. Эмуляция множественного наследования
M: Item 25. Emulating Multiple Inheritance

Динамическое приведение D* к B2* не работает

И принципиально не будет работать, т.к. в алгоритме работы dynamic_cast заложены знания обо всех иерархиях классов программы. Те же взаимоотношения, которые в них не входят всегда останутся за рамками понимания dynamic_cast.


Стр.220: Задача 4.1. Минимизация зависимостей времени компиляции. Часть 1
E: Item 26. Minimizing Compile-time Dependencies-Part 1

Раньше мы могли бы просто заменить "#include <ostream>" строкой "class ostream;"...

А теперь уже не можем, т.к. в процессе стандартизации C++ эта возможность "затерялась". А между тем, это довольно чувствительная потеря, т.к. возможность использования указателей и ссылок на незавершенные типы классов (ввода/вывода) библиотеки C++ широко использовалась. И появление iosfwd является не более чем примитивным хаком (hack), но никак не решением общей проблемы.


Стр.222: Задача 4.2. Минимизация зависимостей времени компиляции. Часть 2

... требуют полного определения C в случае настройки list<C>.

Нет, ну вы видели?! Перевести устоявшийся технический термин "инстанциирование" словом "настройка" -- это надо уметь...


Стр.223: Задача 4.2. Минимизация зависимостей времени компиляции. Часть 2
E: Item 27. Minimizing Compile-time Dependencies-Part 2

Каким же образом можно изолировать клиента от деталей реализации?

Правильный ответ: путем использования интерфейсов! Под интерфейсами, естестенно, понимаются абстрактные базовые классы.


Стр.224: Задача 4.2. Минимизация зависимостей времени компиляции. Часть 2
E: Item 27. Minimizing Compile-time Dependencies-Part 2

Например: "class Map { private: struct MapImpl* pimpl_; };".

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

class X {
 private:
      struct XImpl* pimpl_;
};
и
class X {
 private:
      struct XImpl;
      XImpl* pimpl_;
};
Дело в том, что они не эквивалентны, т.к. в первом случае предварительно объявляется глобальная struct XImpl, а во втором -- вложенная struct X::XImpl.

Стр.227: Задача 4.4. Брандмауэры компиляции
E: Item 29. Compilation Firewalls

Думаю, что не лишним будет отметить, что при использовании интерфейса вместо "идиомы Pimpl" обсуждаемые в данной задаче тонкие вопросы вообще не возникают.

Стр.228: Задача 4.4. Брандмауэры компиляции

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

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


Стр.233: Задача 4.5. Идиома "Fast Pimpl"
E: Item 30. The "Fast Pimpl" Idiom

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

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

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


Стр.240: Задача 5.2. Поиск имен и принцип интерфейса. Часть 2

this может иметь тип X*, const X*, const volatile X*, volatile X*. То, что к типу указателя часто ошибочно добавляют лишний const, связано с тем, что в выражениях this не может быть lvalue.

Достаточно интересное замечание. Дело в том, что this является не указателем, а выражением типа X*. В силу чего он не является lvalue (в частности, мы не можем получить адрес this) и демонстрирует "константные свойства" не будучи указателем-константой. Для ясности, обращение к this можно представлять себе в виде вызова некоей функции, возвращающей X*.

Но это все с формальной точки зрения, т.к. на практике можно полагать, что this имеет тип X* const, т.е. является указателем-константой. В частности, приведенный ниже код выберет void f(const APtr&).

240.cpp
#include <stdio.h>

typedef struct A* APtr;
void f(APtr&)       { puts("f(APtr&)"); }
void f(const APtr&) { puts("f(const APtr&)"); }

struct A { void g() { f(this); } };

int main()
{
 A a;
 a.g();  // печатает f(const APtr&)
}


Стр.241: Задача 5.2. Поиск имен и принцип интерфейса. Часть 2

Это стандартная технология с использованием дескриптора для записи объектно-ориентированного кода в языке, в котором нет классов.

Ну как вам фразочка? Это еще что, совсем смешно становится, когда заглянешь в оригинал: "This is the standard "handle technique" for writing OO code in a language that doesn't have classes", т.е. "Это стандартная техника использования дескрипторов для написания ОО кода в языке, не поддерживающем классы".

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


Стр.243: Задача 5.2. Поиск имен и принцип интерфейса. Часть 2
E: Item 32. Name Lookup and the Interface Principle-Part 2

Надеюсь, после этого вам стала совершенно очевидна важность поиска Кёнига.

Надеюсь, что вы не поддались на провокацию и вам стала "совершенно очевидна важность поиска Кёнига" всего лишь для перегруженных операторов. Фактически, поиск Кёнига возник из попытки частично сгладить проблемы крайне неудачного определения способа перегрузки операторов в C++, ставшей особенно очевидной после добавления в язык пространств имен. А для всего остального поиск Кёнига принес гораздо больше вреда, чем пользы.


Стр.245: Задача 5.2. Поиск имен и принцип интерфейса. Часть 2

Итак, что же произошло?...

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

"Оппаньки! Но ведь смысл пространств имен состоит в предотвращении коллизий имен?!" -- скажете вы. -- "Однако добавление функции в одно пространство имен нарушает работоспособность кода в совершенно другом пространстве". Да, код пространства имен B ломается только потому, что он ссылается на тип из A. Код B нигде не содержит using namespace A;, он даже не содержит using A::X;.

Как видите, ассы пера и словаря трудятся в поте лица заменяя одни слова другими и выкидывая не понравившиеся предложения. Цирк.

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


Стр.259: Задача 6.1. Управление памятью. Часть 1
E: Item 35. Memory Management-Part 1

Достаточно важно различать кучу и свободную память...

Здесь автор позволил себе некоторую вольность закрепив free store за new, а heap за malloc(), т.к. в реальной жизни free store и heap суть синонимы и обозначают динамически выделяемую память. Важно просто различать память полученную от new и от malloc().

И даже более того: если ваше приложение использует динамически загружаемые модули (напр. dll), то может статься, что память, выделенную в одном модуле (напр. при помощи new) нельзя освобождать в другом модуле (при помощи delete), т.к. модули слинкованы с разными версиями функций, использующими разные списки свободной памяти. Такое поведение, очевидно, запрещено стандартом, но в реальной жизни встречается и приводит к чрезвычайно трудноуловимым ошибкам, безмолвно превращая корректный код в некорректный.


Стр.265: Задача 6.3. Применение auto_ptr. Часть 1
E: Item 37. AUTO_PTR

... но далеко не все постоянно применяют его. Это досадно, так как auto_ptr в состоянии решить многие из распространенных проблем C++...

На самом деле, это просто счастье, что auto_ptr мало кем применяется, т.к. его больные на голову создатели умудрились протащить в стандарт класс, копии объектов которого не равны оригиналу! И даже хуже того, auto_ptr -- это единственный "умный указатель", определенный в стандарте, и тот э... плохой. Что могут подумать люди?!

К счастью, в природе встречаются чрезвычайно полезные умные указатели, чью помощь в написании корректного exception safe кода трудно переоценить. Наиболее полезными из них являются разделяемые умные указатели с подсчетом ссылок. Самым известным представителем данного семейства, безусловно, является boost::shared_ptr (www.boost.org), он предоставляет столько возможностей, что его сложность находится на грани разумного. К его наиболее существенным недостаткам стоит отнести то, что он является неотъемлемой частью boost, поэтому его использование в программе влечет к включению великого множества других частей boost, со всеми вытекающими последствиями.

Поэтому мной был создан простой и понятный класс sh_ptr (файл sh_ptr.hpp в исходном коде), который берется только за одну задачу, но делает ее хорошо (кстати, это один из тех прекрасных принципов программирования, которые позабыли разработчики boost). Думаю, что лучше явно подтвердить, что в отличие от уродца-auto_ptr использование умных указателей со стандартными контейнерами не только не запрещено, но и всячески приветствуется из-за существенного повышения безопасности и удобства. Так, например, конструкции vector<sh_ptr<T> > довольно часто встречаются в моем коде, в то время как vector<auto_ptr<T> > уже грубейшая ошибка.


Стр.272: Задача 6.3. Применение auto_ptr. Часть 1
E: Item 37. AUTO_PTR

// Наконец-то верно!
auto_ptr<String> f()
{
 auto_ptr<String> result=new String;

Ох уж мне эти учебные примерчики! Во-первых, при работе со стандартным вводом/выводом C++ о строгой гарантии можно сразу же забыть, т.к. в поток может быть выведена только часть информации, и если после этого возникнет исключение, то откат частично выведенной информации будет никак не возможен.

А во-вторых, и это уже просто смешно, автор забыл, что auto_ptr имеет explicit конструктор, поэтому приведенная им инициализация вызовет ошибку компиляции. Нужно сразу вызывать конструктор: auto_ptr<String> result(new String);.


Стр.283: Задача 6.6. Интеллектуальные указатели-члены. Часть 2
M: Item 31. Smart Pointer Members, Part 2: Toward a ValuePtr

Копирование и присваивание ValuePtr разрешено и имеет семантику создания копии принадлежащего ValuePtr объекта Y с использованием виртуального метода Y::Clone() при наличии такового или конструктора копирования Y в противном случае.

Я бы вам рекомендовал ни в коем случае не придерживаться подобного рода тактики в реальном коде. Дело в том, что копирование объектов имея на руках всего лишь указатель на (возможно базовый) класс -- штука чрезвычайно тонкая. Автоматизация подобного рода операции -- это активный поиск неприятностей, т.к. код ValuePtr может обнаружить, что в классе нет "правильно оформленной" Y* Y::Clone() (в то время как есть sh_ptr<Y> Y::clone()) и попытаться самостоятельно создать объект класса Y (в то время как Y -- абстрактный класс).

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


Стр.299: Задача 7.3. Отложенная оптимизация. Часть 2
M: Item 14. Lazy Optimization, Part 2: Introducing Laziness

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

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

Надеюсь, что участь сия вас миновала, и вы понимаете, что наилучший способ создания из X "класса со счетчиком ссылок" -- это sh_ptr<X>. Ну а код, изменяющий значение (возможно разделенной) строки, выглядит следующим образом:

// создаем личную копию, если нужно
template <class T>
void unshare(sh_ptr<T>& ptr)
{
 if (ptr.refs()>1) ptr=sh_ptr<T>(new T(*ptr));
}

void f(sh_ptr<String>& str)
{
 // ...
 if (собрались изменять) {
    unshare(str);
    *str="изменяем";
 }
}

Стр.309: Задача 7.5. Отложенная оптимизация. Часть 4
M: Item 16. Lazy Optimization, Part 4: Multithreaded Environments

В последней задаче мини-серии рассматривается влияние безопасности потоков на копирование при записи.

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

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


Стр.321: Задача 8.1. Рекурсивные объявления
M: Item 32. Recursive Declarations

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

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

// интерфейс -- состояние конечного автомата
class State {
 public:
      // обработка и возврат изменившегося состояния
      virtual sh_ptr<State> change(const string& input)=0;
      // обязательный виртуальный деструктор
      virtual ~State() {}
 private:
      // запрещаем копирование
      State& operator=(const State&);
};

// состояния -- реализации интерфейса
class StartState: public State { /* */ };
class S2State:    public State { /* */ };
class StopState:  public State { /* */ };
class ErrorState: public State { /* */ };

// объекты-состояния
sh_ptr<State> start(new StartState), s2(new S2State), stop(new StopState),
  error(new ErrorState);

// реализация функции change для состояния Start
sh_ptr<State> StartState::change(const string& input)
{
 if (input=="a") return s2;
 else if (input=="be") return stop;
 else return error;
}
Ну а в реальном коде вместо длинных последовательностей else/if используются таблицы состояний/переходов, например map<string, sh_ptr<State> > (за отсутствием стандартного hash_map).

Стр.324: Задача 8.2. Имитация вложенных функций
M: Item 33. Simulating Nested Functions

В каких случаях вложенные функции могут оказаться полезными и как сымитировать их в C++?

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

А для тех, кто хочет узнать, почему в C/C++ нет вложенных функций объясняю:


Стр.325: Задача 8.2. Имитация вложенных функций

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

На самом деле, сответствующий раздел стандарта имеет номер 14.3.1p2 и написано там следующее: "Локальный тип, тип без компоновки (no linkage), неименованный тип или тип, составленный из любого из этих типов, не может использоваться в качестве аргумента для параметра шаблона".


Стр.344: Задача 9.4. Времена жизни объектов. Часть 2

В нашем случае этот код превращается в следующий.

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

В этом случае мы получим:

class U : T { /* ... */ };
U& U::operator=(const U& other)
{
 T::operator=(other);
 // ... а здесь выполняем присваивание членов U ...
 //   ... оппаньки, а ведь мы уже не U!
 return *this;  // те же оппаньки
}
Как уже было отмечено, вызов T::operator=() безмолвно портит весь последующий код (и присваивание членов U и return). Проявляется же это, чаще всего, в виде таинственной и трудной для отладки ошибки исполнения, т.к. деструктор U не уничтожает свои данные-члены.

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

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

Стр.346: Задача 9.4. Времена жизни объектов. Часть 2
E: Item 41. Object Lifetimes-Part 2

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

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

T& operator=(const T& other)
{
 T temp(other);
 Swap(temp);
 return *this;
}
еще более жестоко "извращает смысл конструирования и деструкции", т.к. время жизни не только начинается и заканчивается, но и в процессе присваивания задействовано уже целых три объекта с пересекающимся временем жизни! Увы, налицо острая некогерентность мышления.

Стр.355: Задача 10.3. Корректность const
E: Item 43. Const-Correctness

При возврате из функции объектов не встроенных типов по значению желательно возвращать константные ссылки.

Думаю, что данная рекомендация внимания не заслуживает, т.к. аргумент, что это поможет дураку не написать что-то вроде poly.GetPoint(i)=Point(2,2); нельзя назвать серьезным. Законы Мерфи утверждают, что

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

Стр.361: Задача 10.4. Приведения
E: Item 44. Casts

pb=(B*)&c1;
Вместо этого приведения воспользуйтесь reinterpret_cast:
pb=reinterpret_cast<B*>(&c1);

Просто класс! Правильный совет: убедитесь, что классы B и C никак не связаны и никогда так не делайте!


Copyright © С. Деревяго, 2003-2009

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