Оглавление по оригиналу | |
Введение | |
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 |
Ну а теперь про перевод Герб Саттер "Решение сложных задач на C++", на страницы которого и ссылается данный материал. Наиболее точной характеристикой качества перевода является фраза "быдло все стерпит" (и это еще политкорректный вариант первоначального впечатления!). Буквально до самого последнего момента я хотел написать, что господ, издающих переводы классических книг подобного качества, следует считать негодяями, но все же решил этого не делать, т.к. они вообще недостойны упоминания. Будет лучше, если вы просто "проголосуете рублем" за их творения, а посему советую вам повнимательнее вглядеться в "список причастных" на реквизитах издательства дабы при случае обходить их стороной.
Увы, но подавляющее большинство наших программистов все еще не может позволить себе купить оригиналы "Excetional C++" и "More Excetional C++", а те, кто не владеет английским, просто вынуждены "наслаждаться переводом". К счастью, (по подтвержденным слухам) в Рунете можно найти электронные варианты обоих книг на английском языке. Так что мои комментарии, первоначально ориентированные только на перевод, в настоящее время можно разделить на два класса:
Ну и, напоследок, маленький совет: если вы хоть немного понимаете по-английски, то можете смело браться за оригинал, т.к. небольшое непонимание художественных оборотов автора все же гораздо лучше технического невежества переводчиков. Не говоря уже о возможности дополнительной языковой практики.
С уважением, Сергей Деревяго.
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")); }Думаю, что кроме указанных автором ошибок с итераторами стоит указать и на ошибки в "общечеловеческом смысле":
vector<Date>
используется слишком часто, чтобы не ввести соответствующий "локальный" typedef
:
typedef vector<Date> vec; vec e; vec::iterator first = ...Не стоит пренебрегать подобного рода мелочами, т.к. они позволяют не только сохранить несколько лишних нажатий на клавиши, но и вносят в код дополнительную ясность, убирая бесполезный для понимания "шум".
copy(istream_iterator ...)
и copy(... ostream_iterator)
крайне не рекомендуется к практическому использованию, т.к. любой практически полезный ввод/вывод должен уметь правильно обращаться со всеми возникающими ошибками, плюс должна быть возможность для (относительно) безболезненного изменения формата входных/выходных данных. Как не трудно видеть, копирование посредством i/ostream
итераторов данными свойствами не обладает.
Тем не менее, подобного рода код очень любят приводить в качестве учебных примеров. Для меня всегда было загадкой зачем учить людей тому, что они не должны использовать на практике.
find
для поиска в контейнере путем последовательного перебора (в среднем) половины его элементов является еще одним дурным примером, чрезвычайно обожаемым авторами. Сказать, что это может быть неэффективно в программах, работающих с реальными (т.е. сколь угодно большими) данными -- это еще ничего не сказать!
Отговорки, что профессиональный программист никогда не напишет такой код категорически не принимаются. Как это "не напишет", если абсолютное большинство книг просто кишит подобного рода примерами?! Мне приходилось видеть, как отнють не глупые профессионалы с немалым стажем работы пробовали сортировать элементы map
с помощью "стандартного подхода" sort(m.begin(), m.end())
. Как говорится, угадайте почему...
Мои строки, вообще говоря, нулевые символы содержат, т.к. это не запрещено стандартом. И даже более того, некоторые мои приложения действительно вынуждены работать со строками, состоящими из любых символов, включая ноль ('\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
. Тем более, что понятие "нечувствительный к регистру" разными людьми понимается по-разному. И существуют языки, где приведение к другому регистру изменяет количество букв в слове или даже вообще невозможно из-за отсутствия больших и/или маленьких букв.
К сожалению, перевод данной части книги, а также соответствующего текста из стандарта выполнялся некомпетентными людьми, поэтому без соответствующей подготовки довольно трудно понять что же на самом деле имелось в виду. А имелось в виду следующее.
Во-первых, вот перевод сноски 106 параграфа 12.8p2: Т.к. конструктор-шаблон никогда не является конструктором копирования, наличия такого шаблона не подавляет неявный конструктор копирования. Конструкторы-шаблоны участвуют в выборе перегруженного конструктора наравне с другими конструкторами, включая конструкторы копирования, и конструктор-шаблон может быть использован для копирования объекта, если он обеспечивает лучшее соответствие, чем остальные конструкторы.
Т.е. наличие конструкторов копирования-шаблонов и операторов присваивания-шаблонов не подавляет соответствующие функции-члены, генерируемые компилятором по умолчанию.
Ну что ж, давайте посмотрим. Только есть одно "но": мы будем смотреть стоит ли этим вообще заниматься.
Даже невооруженным взглядом видно, что код стал существенно сложнее, при этом возросли накладные расходы и упала скорость работы. А что мы выиграли? Была ли пользователям действительно нужна эта более строгая гарантия такой ценой, если она может быть реализована самими пользователями только там, где это нужно?
А теперь посмотрим на накладные расходы, возникающие там, где незнакомый с ассемблером программист их совершенно не ожидает. Рассмотрим следующий упрощенный пример:
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%.
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). Для приемлемого по скорости поиска нужно использовать специально для этого предназначенные структуры данных и/или алгоритмы.
Скажу сразу, что на платформах, где флаг ios::binary
имеет смысл (например Windows) это невозможно, т.к. стандартные потоки ввода/вывода cin
и cout
открываются startup кодом в текстовом режиме, так что при вызове echo <infile >outfile
выходной файл, вообще говоря, не будет совпадать со входным.
К тому же, используемый автором флаг ios::binary
для варианта echo infile outfile
приведет к тому, что результат работы будет отличаться от echo <infile >outfile
.
Т.о. правильная формулировка задания должна звучать так: "... повторяет вводимую текстовую информацию и одинаково работает ...", а код не должен использовать ios::binary
.
Ну что же, еще раз скажем переводчикам спасибо за наше счастливое детство...
Дорогие переводчики! Слово single в данном контексте означает единственной, а не единой. Не стоит останавливать поиск в словаре на первом попавшемся значении.
Итак, автор имел ввиду, что каждая часть кода должна отвечать за свою, одну-единственную задачу (и делать это хорошо).
Далее. В оригинале на этом месте стоит рекомендация: "Prefer encapsulation. Separate concerns", т.е. "Предпочитайте инкапсуляцию. Разделяйте ответственность". Каким образом она превратилась в приведенную выше "Каждая часть кода..." рекомендацию из "E: Item 12. Writing Exception-Safe Code-Part 5" остается загадкой. Вероятно, некстати подвернулась неудачная фаза Луны.
remove()
Removesstd::vector<int>
.
К сожалению, автор опять ничего не сказал о том, что удаление произвольных элементов из std::vector
-- это не очень удачная идея. Почти такая же плохая, как и использование функции std::remove
.
const
-функции.
Имеется ввиду, что предикат не должен пытаться изменить элемент контейнера, передаваемый ему в качестве параметра.
Не устаем благодарить дорогую редакцию за дословный перевод. Без всякой претензии на понимание смысла.
T::Clone()
без параметров будет успешной и в том случае, когда Clone()
имеет параметры по умолчанию ...
Вспоминая известный анекдот так и хочется спросить: "Вам шашечки или ехать?" Ох уж мне эти учебные примеры!
Дело в том, что задача в такой постановке лишена практического смысла, т.к. в реальном мире мы просто напишем ptr=t.Clone()
, а то, что у Clone()
могут быть параметры по умолчанию нас нисколько не должно волновать, коль скоро вызов компилируется и делает то, что нам нужно.
К тому же, рядом с чрезмерной строгостью к наличию/отсутствию параметров по умолчанию находится неоправданное легкомыслие по отношению к правам доступа к T::Clone()
. Согласитесь, что ситуация, когда столь желанная T* T::Clone()
существует, но недоступна не многим лучше, чем просто ее отсутствие.
А в следующем разделе я покажу, как игнорирование прав доступа вместо выбора подходящего кода (на стадии компиляции) приводит к ошибке компиляции.
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>
не является его другом, то налицо попытка нарушить права доступа. Получаем ошибку компиляции.
class IsDerivedFrom2 { static void Constraints(D* p) { B* pb = p; pb = p; }И здесь мы видим все ту же проблему: если класс
D
использует закрытое (или защищенное) наследование и не объявит IsDerivedFrom2<D, B>::Constraints()
другом, то в момент использования наследования от IsDerivedFrom2<D, B>
нас будет ждать все та же ошибка компиляции.
Кстати сказать, тема наложения ограничений на параметры шаблонов весьма обширна и интересна, и борьба с правами доступа является всего лишь одним из ее аспектов. Одним, но важным.
Исторически, необходимость проверки данных ограничений выросла из того факта, что современные шаблоны чрезвычайно интенсивно используют вспомогательные шаблоны, которые могут пытаться использовать отдельные особенности своих параметров (напр. вызывать все ту же T::Clone()
). И если же эдак на четвертом-пятом вложенном шаблоне обнаруживается недостача столь милой сердцу особенности (напр. обнаружено наличие отсутствия T::Clone()
), то внешний вид выданного компилятором сообщения об ошибке способен э... поколебать эмоциональное равновесие даже самых спокойных программистов.
И именно для исключения необходимости чтения нескольких страниц текста сообщения об ошибке появилась тенденция проверки нужных свойств параметров шаблона как можно раньше, что существенно упрощает понимание сути ошибки.
Так вот, тонкости с правами доступа состоят в том, что код проверяющий ограничения и код их использующий, как правило, обладают разными правами доступа к проверяемым параметрам. Поэтому ошибки неправильной диагностики и/или использования становятся весьма вероятными.
Как не трудно видеть, результатом работы приведенного автором кода будет Прелесть!Прелесть!
"А где же шутка"? -- спросите вы? Шутка в том, что в английском языке существует пословица о том, что "роза всегда хорошо пахнет"... [истерический хохот в зале]
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); // а здесь уже точно приплыли }Короче, еще один "учебный пример"...
Признаюсь честно: такие "правила" на меня действуют как красная тряпка на быка. Что значит никогда не оптимизируй?
Судя по всему, развернутый смысл этих правил таков: "Да, я знаю, что ты просто тупой кодировщик, ведь мы тебя поймали, когда ты спустился с гор за спичками, погрузили в вагоны и назвали программистами. Поэтому сделай хоть что-нибудь! Пока мы монополисты, мы можем впаривать все что угодно, тем более, кто купит новый компьтер у наших партнеров, если новая программа потребляет так мало ресурсов, что сгодится и старый? И кто купит наши бесконечные новые версии, в которых новым является лишь только внешний вид да исправление самых наболевших ошибок (с добавлением новых на будущее)?".
К счастью, сам автор понимает всю абсурдность подобных "правил" и в конце раздела советует: оптимизируйте с умом. Сам я обычно начинаю с выбора наиболее подходящих алгоритмов и структур данных, что, как правило, действительно позволяет впоследствии избежать необходимости "оптимизации". Те же, кто использует, например, столь милый сердцу многих авторов (линейный) std::find
по контейнеру с реальными объемами данных могут быть уверены, что без "оптимизации" не обойтись.
Но можно ли назвать это оптимизацией, если с самого начала было видно, что цена поиска O(N)
не годится для N
реальных данных? В конце концов, мы же не идем к проктологу, когда у нас болят зубы? Последующий выбор стоматолога вместо проктолога вряд ли эквивалентен замене стоматолога-практиканта на стоматолога-профессора, хотя и в том и в другом случае мы меняли врача на врача.
vector
и deque
vector
and deque
deque
нет функций-членов capacity()
и reserve()
, которые есть у вектора, но это нельзя считать большой потерей; на самом деле эти функции -- недостаток vector
...
Как сказал бы один наш преподаватель: "Ну вы, Герб, крепко рубанули!" Ничего себе недостаток -- возможность заранее указать размер непрерывной области памяти, скоро понадобящейся вектору (надеюсь, вы понимаете, что std::vector
преднамеренно использует непрерывный блок памяти для ускорения доступа по индексу).
За что действительно можно попинать дизайн вектора, так это за отсутствие функции типа shrink()
для эффективного освобождения ненужной памяти, но этот факт вызван другим просчетом, относящемся ко всей STL в целом -- отсутствие функции reallocate()
(аналога сишного realloc()
) в интерфейсе используемого контейнерами аллокатора.
set
и map
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
, идущий вразрез с общепринятой идеологией итераторов, но допускающий более эффективную реализацию.
Здесь имеется ввиду, что контейнеры будут одновременно и exception-safe и exception-neutral. Хоть из exception-neutral и следует exception-safe, но обратное, вообще говоря, неверно, т.к. вместо перевозбуждения оригинального исключения можно возбудить другое (например, содержащее сообщение о текущем контексте ошибки плюс информацию об оригинальном исключении).
Браво, господа переводчики! Браво!
Это же как надо постараться, чтобы из простого "exception safety affects your class's design" соорудить такую тавтологию?!
Что же она нам действительно гарантирует? На самом деле, только то, что все объекты, изменявшиеся кодом возбудившей исключение функции, остались в корректном состоянии (их инварианты не нарушены), но изменились непредсказуемым образом (напр. контейнер мог потерять часть своих объектов и/или приобрести новые; тем не менее, это не привело к утечке ресурсов). Самое лучшее, что мы можем после этого предпринять -- это уничтожить (потенциально) изменившиеся объекты, или присвоить им новые значения.
void f(B&); try { f(b); } catch (...) { // считаем, что содержимое объекта b изменилось }
Самое сложное в этой гарантии -- это понимание того, что если обеспечение basic guarantee не привело автоматически к strong guarantee, то и не нужно пытаться ее обеспечить. Дело в том, что обеспечение strong guarantee "вручную" производится путем введения дополнительных копий объектов, что чаще всего приводит к ненужным накладным расходам, т.к. пользователь, как правило, и не рассчитывал воспользоваться "строгостью" гарантии, а просто позволит процессу раскрутки стека уничтожить все изменявшиеся объекты и запустит другую операцию, отметив где-то, что предыдущая не удалась.
Там же, где strong guarantee действительно нужна, пользователь может обеспечить ее самостоятельно (естественно, опираясь на базовые гарантии, которые обязаны присутствовать всегда):
void f(B&); B b2(b); // делаем копию try { f(b); } catch (...) { // содержимое b изменилось, но у нас есть копия b2 }
StackImpl
?
Если кратко и по сути, то собственно вспомогательный класс, а не набор вспомогательных функций, нам нужен из-за его деструктора. Вызовы деструктора будут автоматически вставляться компилятором, освобождая нас от обременительных блоков try/catch
. Да, это техника RAII (resource acquisition is initialization).
/*????*/
...
Вы будете смеяться, но господа переводчики умудрились напутать даже с переводом времен глаголов, в результате чего описание двух методов решения стало маловразумительным. Чувствую, что придется переводить самому...
/*????*/
спецификатором доступа может быть или protected
или public
. (Если бы им был private
, то никто не смог бы воспользоваться классом.) Рассмотрим сначала, что получится, если мы выберем protected
.
Выбор protected
означает, что StackImpl
предназначен для использования в качестве закрытого базового класса. Так что Stack
будет "реализован посредством" StackImpl
, что собственно и означает закрытое наследование, и мы имеем ясное разделение ответственностей. Базовый класс StackImpl
будет заботиться об управлении буфером и о уничтожении всех оставшихся объектов T
в процессе уничтожения объекта Stack
, в то время как производный класс Stack
будет заботиться о создании всех объектов T
в выделенной памяти. Все управление памятью происходит за пределами класса Stack
, т.к. начальное выделение памяти должно успешно завершиться еще до того, как будет вызван конструктор Stack
. В следующей задаче мы реализуем именно этот вариант.
public
.
Использование public
подсказывает, что StackImpl
предназначен для использования в качестве структуры данных некоторым внешним клиентом, т.к. его данные-члены общедоступны. Так что Stack
снова будет "реализован посредством" StackImpl
, только на этот раз с использованием HAS-A отношения включения вместо закрытого наследования. И мы снова имеем то же самое четкое разделение ответственностей. Объект StackImpl
будет заботиться об управлении буфером и о уничтожении всех оставшихся объектов T
в процессе уничтожения объекта Stack
, а объемлющий объект Stack
позаботится о создании объектов T
в выделенной памяти. Т.к. данные-члены инициализируются перед входом в тело конструктора, то управление памятью также происходит за пределами класса Stack
, т.к. начальное выделение памяти обязано успешно завершиться еще до того, как будет вызван конструктор Stack
.
Здесь имеется ввиду техника RAII (resource acquisition is initialization), обычно переводимая как "выделение ресурса есть инициализация". А захватывают ресурсы только лживые ястребы империализма.
А сейчас, после того, как вы над ним вволю подумали, давайте подумаем еще раз. На тему того, стоит ли всегда использовать этот изящный способ определения оператора присваивания.
Я не буду проводить детальный анализ всех возможных случаев, т.к. тема эта весьма глубока и объемна (начиная с того, что большинство классов вообще не должно иметь конструктора копирования и оператора присваивания). На примере класса Stack
мы рассмотрим всего лишь один частный вопрос: стоит ли имея строгую гарантию на конструктор копирования (и эффективную, не возбуждающую исключений функцию Swap()
) определять оператор присваивания вышеуказанным образом, автоматически получая строгую гарантию и на него.
Для проведения реальных измерений производительности я написал класс Stack2
, отличающийся от оригинала реализациями оператора присваивания и функции Push()
.
Вместо уничтожение старых элементов (посредством деструктора) и создания на их месте новых (с помощью конструктора копирования), новая реализация использует оператор присваивания. Фактически, оператор присваивания должен произвести те же операции и в той же последовательности, но т.к. он точно знает, что за уничтожением старого содержимого объекта сразу же последует создание нового, то реализация оператора присваивания может быть более эффективной.
По сути, код оператора присваивания сводится к следующему циклу:
for (int i=0; i<N; i++) v_[i]=other.v_[i];Но т.к. участвующие в присваивании стеки могут содержать разное количество элементов, нам придется учесть все возможные варианты, что сильно усложняет картину:
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; }А сейчас, попробуем разобраться, что именно означает базовая гарантия для приведенного кода. Все места, которые могут возбудить исключение, выделены жирным шрифтом.
operator new
, в результате чего стек, которому присваивали новое значение, потеряет все свои элементы.
T::operator=
в процессе работы цикла. В результате чего, одна часть стека будет содержать новые элементы, другая -- старые, а между ними будет находиться элемент с неопределенным значением.
T
внутри construct()
. На выходе мы получим укороченный вариант стека, значение которого мы хотели присвоить.
Может ли код пользователя надежно различить все эти случаи, вместе с их всевозможными подвариантами? Правильный ответ: а он и не должен этого делать! Если старое значение стека, которому присваивается значение новое, настолько ему дорого, то он всегда может заготовить копию на случай возникновения исключения, дабы обеспечить себе строгую гарантию своими руками.
Push()
являлась проверка того, насколько возрастает производительность, если простой и красивый код, использующий технику RAII, заменить сложным, оптимизированным. По сути, новый код делает то же самое, но без использования вспомогательных объектов. Как следствие, мы имеем ту же гарантию.
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_++; }
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% |
Анализ:
Stack<string>
, первая реализация) получено замедление работы на 30%. Вот вам и оптимизация!
T& operator=(const T& t) { T tmp(t); swap(tmp); return *this; }как правило, не приводит к существенным накладным расходам. А его простота и безопасность делают его крайне привлекательным для практически повсеместного использования.
В оригинале: ... 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?)
А вот стоит ли это считаеть достоинством -- вопрос спорный. Как я уже объяснял, оператор присваивания может работать быстрее, чем последовательные вызовы деструктора и конструктора копирования. Стоит ли избегать вызова потенциально более эффективной операции, эмулируя ее поведение обходным путем?
Не удивлюсь, если окажется, что иногда все-таки стоит. Как правило, добавление еще одного параметра шаблона (с удачно выбранным) значением по умолчанию является прекрасным решением подобного рода проблем: template <class T, class UseAssign=Yes> class Stack
. Если использование оператора присваивания нежелательно, то пользователь сможет его отменить: Stack<string, No>
.
Stack
?
Спецификации исключений в том виде, как это определено в стандарте не только бесполезны, но и вредны. Более подробно об этом можно почитать в статье автора A Pragmatic Look at Exception Specifications.
Вопрос в том, что именно считать "корректной обработкой нескольких исключений"? Из того, что мы имеем на сегодняшний день, можно сделать вывод о том, что, грубо говоря, в C++ мы всегда можем возбудить исключение; после чего начинается раскрутка стека, в процессе которой языком вызываются деструкторы подлежащих уничтожению объектов. Так вот, эти самые деструкторы не имеют право выбрасывать исключения за свои пределы, т.к. иначе получится "пересечение" двух исключений и, как следствие, завершение работы посредством вызова std::terminate()
.
Т.о. мне представляется корректным такой дизайн функции destroy()
, при котором:
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
. Т.о. если повторное исключение и возникнет, то это произойдет в деструкторе объекта, вызванного во время раскрутки стека. К сожалению, то, что объект запоминает ссылку на локальную переменную приводит к ухудшению генерируемого кода на большинстве реализаций, т.е. за повышенную безопасность приходится платить.
EvaluateSalaryAndReturnName()
имеет два побочных действия.
Здесь автором допущена очевидная смысловая ошибка: возврат строки побочным действием (side effect) не является. Крайне неудачный учебный пример при иллюстрации достаточно серьезных понятий.
Вообще говоря, это не так. Дело в том, что бывают случаи, когда исключение выбрасывается только при передаче некорректных аргументов (напр. при попытке обращения за пределы допустимых значений индекса массива). При таких условиях функция действительно может возбуждать исключения, но только при ошибках в коде пользователя (logic error). Т.е. корректный код может указать пустую спецификацию исключений даже если базовые конструкторы ее не имеют.
А теперь маленькое философское отступление. Те, кто писал действительно серьезный код знают, что исключений, возбуждаемых при ошибках программиста (напр. std::logic_error
, который определяется стандартом как: для извещения об ошибках, которые, предположительно, могут быть обнаружены до выполнения программы) быть не должно. Самое лучшее, что мы можем сделать при обнаружении подобного рода ошибки -- это сразу же прервать выполнение программы, сгенерировав информацию, необходимую для определения сути происшедшего (напр. core dump). Дело в том, что "ошибка здесь" является, как правило, следствием "ошибки там" и продолжение исполнения некорректного кода в надежде на лучшее приведет к Ужасным Последствиям (напр. сумма, эквивалентная нескольким десяткам лет зарплаты программиста, будет зачислена на неизвестный счет где-нибудь в Буркина-Фасо. Но это, конечно, маловероятно, т.к. чаще всего обнаруженное несоответствие двойной бухгалтерии приводит к временной остановке всех операций до выяснения причин; а убыток от нескольких часов простоя серьезного банка приводит к гораздо более дорогостоящим последствиям...).
Браво, господа-переводчики, браво! Это же сколько э... о! "технической смелости" и "непробиваемого природного оптимизма" надобно иметь, чтобы вполне устоявшийся термин раскрутка стека (stack unwinding) перевести как свертка стека?!
Особую пикантность этому, безусловно правильному, совету придает использование std::uncaught_exception()
в стандарте! Цитирую по 27.6.2.3p4: If ((os.flags() & ios_base::unitbuf) && !uncaught_exception())
is true
, calls os.flush()
.
Воистину, верблюд -- это лошадь, созданная комитетом по стандартизации.
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()
, но, судя по всему, данное условие никак нельзя назвать чрезмерно обременительным.
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
.
К озвученным правилам тов. Мейерса так и хочется добавить аналогичное: Каждая хозяйка должна знать, что забитый молотком шуруп держится крепче, чем гвоздь, закрученный отверткой.
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; }На первой фазе присваивания мы пробуем выполнить (в сторонке) всю работу, которая может не пройти. А на второй -- подтверждаем произведенные изменения защищенным от сбоев способом.
Даже не знаю что и сказать... Конечно, знать о explicit
необходимо, но говорить о нем там, где пользователь сознательно рассчитывает на неявное преобразование -- это, как минимум, не очень удачная идея.
Complex
с величинами типа double
, имеет смысл предоставить также перегруженные функции operator+(const Complex&, double)
и operator+(double, const Complex&)
.
Как вы уже, видимо, догадались, удобство здесь не при чем, т.к. вызов x+2
будет воспринят как operator+(x, Complex(2))
. А определение дополнительных операторов, принимающих аргументы типа double
, имеет смысл для повышения производительности.
Наши любимые переводчики в который раз оторвались. Автором имелось ввиду следующее: создайте (закрытую) виртуальную функцию и вызывайте ее из функции-нечлена. Например:
class Base { private: virtual Stream& vprint(Stream&); // да, производные классы могут замещать // даже private виртуальные функции // ... }; inline Stream& operator<<(Stream& s, const Base& b) { return b.vprint(s); }
<<
следует позаботиться о флагах формата вывода, с тем чтобы обеспечить максимальные возможности при его использовании.
Здесь автором имеется ввиду, что переопределяемый оператор <<
должен правильно реагировать на установленные пользователем флаги формата. Например, значение ширины поля сбрасывается в ноль после каждого вывода в поток, так что наивная реализация применит устаноленное значение к "(", а не ко всему "(1,2)".
А хотите знать, что было написано в оригинале? Извольте-с:
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.
Вот так, стараниями дорогих переводчиков простые и понятные высказывания превращаются в словесный понос. Тоска...
Здесь возникает некоторая двусмысленность. Автором написано было следующее: Оно также влияет на использование памяти и производительность путем добавления ненужных виртуальных таблиц и вспомогательного кода для переадресации вызовов к классам, которые в действительности в них не нуждаются.
Еще один понос. В оригинале:
Never inherit publicly to reuse code (in the base class); inherit publicly in order to be reused (by code that uses base objects polymorphically).
Т.е. открытое наследование служит не для использования, а чтобы быть использованным.
GenericTableAlgorithm
нужно добавить:
Например, клиент может создать конкретный производный класс и использовать его следующим образом:
class MyAlgorithm : public GenericTableAlgorithm { // замещаем Filter() и ProcessRow() для реализации специфических действий }; int main() { MyAlgorithm a("Customer"); a.Process(); }
Хоть и положение запятой, как нам известно с детства, может играть весьма существенную роль, вариант использования шаблона абсолютно отпадает по той простой причине, что его использование заставит нас вместо прототипа GenericTableAlgorithm(const string& table, GTAClient& method);
включить в заголовочный файл реализацию данной функции, которая в реальной жизни потянет за собой еще черт знает что.
Да, в стандарте определен мертворожденный export
, который вроде бы и должен был решать подобного рода проблемы. Но на самом деле, как и абсолютное большинство того, что создается (а не узаконивается) комитетами по стандартизации, export
: во-первых, некорректно определен, а во-вторых -- не решает тех задач, чьей проблематикой он обязан своему существованию. Более подробно об этом можно почитать в соответствующих материалах автора Why We Can't Afford Export.
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 } |
т.е. во второй раз вызывается функция базового класса, печатающая изменившееся значение.
void g1(A& x) { cout<<"g1:"<<x.Name()<<endl; }
Естественно, все семейство функций g()
должно принимать аргументы по значению, а не по ссылке.
D*
к B2*
не работает
И принципиально не будет работать, т.к. в алгоритме работы dynamic_cast
заложены знания обо всех иерархиях классов программы. Те же взаимоотношения, которые в них не входят всегда останутся за рамками понимания dynamic_cast
.
#include <ostream>
" строкой "class ostream;
"...
А теперь уже не можем, т.к. в процессе стандартизации C++ эта возможность "затерялась". А между тем, это довольно чувствительная потеря, т.к. возможность использования указателей и ссылок на незавершенные типы классов (ввода/вывода) библиотеки C++ широко использовалась. И появление iosfwd
является не более чем примитивным хаком (hack), но никак не решением общей проблемы.
C
в случае настройки list<C>
.
Нет, ну вы видели?! Перевести устоявшийся технический термин "инстанциирование" словом "настройка" -- это надо уметь...
Правильный ответ: путем использования интерфейсов! Под интерфейсами, естестенно, понимаются абстрактные базовые классы.
// интерфейс A class A { public: // определяемые интерфейсом операции virtual void f()=0; virtual int g()=0; // обязательный виртуальный деструктор virtual ~A() {} private: // запрещаем копирование A& operator=(const A&); }; sh_ptr<A> createA(); // фабрика для создания объектов типа A, возвращающая // "умный указатель" на созданный объект void h() { // создаем объект типа A sh_ptr<A> a=createA(); // и используем a->f(); int i=a->g(); // а по выходу из блока деструктор "умного указателя" корректно его уничтожит }Обратите внимание, что для предотвращения случайной попытки копирования нам достаточно объявить закрытым только оператор присваивания, т.к. пользователь интерфейса не сможет воспользоваться конструктором копирования по умолчанию в силу того, что создавать объекты абстрактного класса запрещено. Если же интерфейс поддерживает копирование скрывающегося за ним объекта, то он должен явно определить соответствующие операции:
class A { public: sh_ptr<A> clone() const =0; // создание копии void assign(const A&)=0; // присваивание // ... };Выбор функции
assign()
вместо определения виртуального оператора присваивания -- это, конечно, дело вкуса. Но я вам советую всегда определять закрытый оператор присваивания в интерфейсах, т.к. в противном случае компилятором будет сгенерирован оператор присваивания по умолчанию, который, очевидно, будет просто пустой операцией, и вызвавший его по недосмотру программист никакой полезной диагностики от компилятора не получит.
B
, использующего класс A
, на интерфейс и реализацию, чье изменение не должно затрагивать пользователей класса. Вся процедура разбита на три шага: 1). представление исходного класса B
; 2). использование "идиомы Pimpl" и 3). использование интерфейса.
A
достаточно тривиально:
a.hpp |
---|
#ifndef __A_HPP__ #define __A_HPP__ // класс A, являющийся деталями реализации класса B struct A { A(int) {} void preDestr() {} // должна вызываться перед уничтожением void f() {} }; #endif |
Вопрос может вызвать только функция preDestr()
: смысл ее появления в том, чтобы показать каким образом можно обращаться к деталям реализации из деструктора класса B
.
b.hpp |
---|
#ifndef __B_HPP__ #define __B_HPP__ #include "a.hpp" // обязаны включить // класс B, не скрывающий детали реализации class B { A a; void privF() { a.f(); } public: B(int i) : a(i) {} B(const B& b) : a(b.a) {} B& operator=(const B& b) { a=b.a; return *this; } ~B() { a.preDestr(); } void f() { privF(); } }; #endif |
Класс B
использует класс A
только для собственных нужд, поэтому детали реализации вроде члена a
и функции privF()
крайне желательно удалить из включаемого файла.
А вот и код пользователя, использующий интерфейс класса B
и ничего не желающий знать о деталях его реализации:
223.cpp |
---|
#include "b.hpp" int main() { B b(1); B b2(b); b=b2; b.f(); } |
b.hpp |
---|
#ifndef __B_HPP__ #define __B_HPP__ // класс B, скрывающий детали реализации class B { struct Internal; // содержит детали реализации Internal* impl; // единственный член-данные класса B public: B(int i); B(const B& b); B& operator=(const B& b); ~B(); void f(); }; #endif |
Как можно видеть, все детали реализации заменены на указатель на внутреннюю структуру, чья реализация находится в соответствующем файле:
b.cpp |
---|
#include "b.hpp" #include "a.hpp" // включаем только в файле-реализации // внутренний класс -- детали реализации класса B struct B::Internal { A a; Internal(int i) : a(i) {} Internal(const Internal& in) : a(in.a) {} void operator=(const Internal& in) { a=in.a; } ~Internal() { a.preDestr(); } void privF() { a.f(); } // переместилась в реализацию }; B::B(int i) : impl(new Internal(i)) { // находящийся здесь код должен позаботиться о том, чтобы не потерять // созданный объект Internal из-за возбуждения исключения } B::B(const B& b) : impl(new Internal(*b.impl)) { // находящийся здесь код должен позаботиться о том, чтобы не потерять // созданный объект Internal из-за возбуждения исключения } B& B::operator=(const B& b) { *impl=*b.impl; return *this; } B::~B() { delete impl; } void B::f() { impl->privF(); } |
Поскольку структура B::Internal
включает член a
, она обязана определять соответствующий набор конструкторов/деструкторов. В нее также переместилась и закрытая функция privF()
, которая была удалена из определения класса B
, как не являющаяся частью его интерфейса.
Отметим, что несмотря на все метаморфозы, происшедшие с классом B
, код пользователя никак не изменился (не приводится). Данный аспект является наиболее сильной стороной "идиомы Pimpl".
b.hpp |
---|
#ifndef __B_HPP__ #define __B_HPP__ // интерфейс B, определяющий все необходимые операции class B { B& operator=(const B& b); // запрещаем public: virtual B* clone() const =0; // аналог конструктора копирования virtual void assign(const B& b)=0; // аналог оператора присваивания virtual ~B() {} virtual void f()=0; }; // фабрика объектов B, аналог конструктора B(int i) B* createB(int i); #endif |
Как можно видеть, все части интерфейса оригинального класса B
превратились в чисто виртуальные функции. Конечно, за исключением конструктора, вместо которого предоставлена фабрика объектов. Стоит отметить, что в реальном коде для реализации фабрики объектов желательно использовать более мощное средство нежели отдельно стоящая функция, т.к. неопределенное количество фабрик-функций, разбросанных по различным включаемым файлам, слабо поддается управлению.
А вот и реализация нашего интерфейса (замечу, что хотя описание класса-реализации сразу в .cpp файле и годится для учебного примера, в реальном коде следует выбрать более масштабируемую стратегию):
bimpl.cpp |
---|
#include "b.hpp" #include "a.hpp" // включаем только в файле-реализации // класс BImpl, реализующий интерфейс B (все члены закрыты) class BImpl : public B { private: virtual B* clone() const { return new BImpl(*this); } virtual void assign(const B& b); virtual ~BImpl() { a.preDestr(); } virtual void f() { privF(); } A a; BImpl(int i) : a(i) {} BImpl(const BImpl& b) : a(b.a) {} void privF() { a.f(); } BImpl& operator=(const BImpl& b); // не реализуем friend B* createB(int i); // для эксклюзивного доступа к конструктору }; // оператор присваивания, принимающий _любую_ реализацию интерфейса B // если вы не владеете полной информацией обо всех возможных вариантах // реализаций по обе стороны присваивания, то лучше вообще не определять // assign() в интерфейсе void BImpl::assign(const B& b) { // считаем, что нам передана ссылка на BImpl, в противном случае будет // возбуждено исключение const BImpl& bi(dynamic_cast<const BImpl&>(b)); a=bi.a; } B* createB(int i) { return new BImpl(i); } |
Класс BImpl
предназначен только для реализации интерфейса B
, поэтому все его члены закрыты, а фабрике объектов явно предоставлен доступ.
Наибольший интерес представляет собой код пользователя, которому пришлось измениться:
223.cpp |
---|
#include "b.hpp" int main() { B* ptr=createB(1); // B b(1); B* ptr2=ptr->clone(); // B b2(b); ptr->assign(*ptr2); // b=b2; ptr->f(); // b.f(); } |
Фактически, мы видим прямое использование указателей, которое скрыто от нас в "идиоме Pimpl". Хорошо это или плохо? С идеологической точки зрения, это дает нам больше гибкости, но и, соответственно, налагает больше ответственности. Примером возросшей гибкости может служить простота реализации функции swap()
: мы просто меняем значения своих же собственных указателей, в то время как "идиома Pimpl" обязана определять функцию-член swap()
. Ну а ответственностью является необходимость корректно удалить созданный фабрикой объект даже в случае возникновения исключений. Простым и элегантным решением данной проблемы является использование умных указателей, которое крайне рекомендуется для практического применения:
class B { // ... virtual sh_ptr<B> clone() const =0; // ... }; sh_ptr<B> createB(int i); int main() { sh_ptr<B> ptr=createB(1); sh_ptr<B> ptr2=ptr->clone(); ptr->assign(*ptr2); ptr->f(); }
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
.
В то время как автором было написано: "... которая пригодна в первую очередь для создания разделяемой реализации с подсчетом ссылок", что уже мало похоже на бред.
Это, конечно, чушь. Начнем с того, что автором имелось ввиду не "неэффективное", а "менее эффективное" использование памяти, что, как говорят в Одессе, две большие разницы. Но даже это утверждение сильно преувеличено, т.к. выделение памяти блоками для элементов фиксированного размера приводит к существенной экономии памяти (особенно для небольших объектов) при весьма существенном увеличении производительности.
Дабы не быть голословным, я включил в исходный код класс fixed_alloc
-- быстрый аллокатор памяти, пригодный для промышленного использования. С его помощью вы можете реализовать операторы new
/delete
ваших классов и оценить реальное влияние на производительность и используемый объем памяти.
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&) } |
Ну как вам фразочка? Это еще что, совсем смешно становится, когда заглянешь в оригинал: "This is the standard "handle technique" for writing OO code in a language that doesn't have classes", т.е. "Это стандартная техника использования дескрипторов для написания ОО кода в языке, не поддерживающем классы".
Да, я тоже думаю, что в процессе непосильного труда товарищ-"оператор машинного перевода" просто отлучился по личным делам, а оставленный без присмотра Промт не справился с управлением. Кстати, видимо я зря ругал качество перевода -- программа, в целом, отработала более чем хорошо...
Надеюсь, что вы не поддались на провокацию и вам стала "совершенно очевидна важность поиска Кёнига" всего лишь для перегруженных операторов. Фактически, поиск Кёнига возник из попытки частично сгладить проблемы крайне неудачного определения способа перегрузки операторов в C++, ставшей особенно очевидной после добавления в язык пространств имен. А для всего остального поиск Кёнига принес гораздо больше вреда, чем пользы.
А произошел очередной идиотизм переводчиков, как всегда, приведший к искажению смысла. Ниже следует правильный перевод абзаца.
"Оппаньки! Но ведь смысл пространств имен состоит в предотвращении коллизий имен?!" -- скажете вы. -- "Однако добавление функции в одно пространство имен нарушает работоспособность кода в совершенно другом пространстве". Да, код пространства имен B
ломается только потому, что он ссылается на тип из A
. Код B
нигде не содержит using namespace A;
, он даже не содержит using A::X;
.
Как видите, ассы пера и словаря трудятся в поте лица заменяя одни слова другими и выкидывая не понравившиеся предложения. Цирк.
Да, думаю нужно отметить, что код в B
"ломается" правильным образом, т.е. выдается ошибка компиляции вместо безмолвного вызова некорректной функции, что не может не радовать.
Здесь автор позволил себе некоторую вольность закрепив free store за new
, а heap за malloc()
, т.к. в реальной жизни free store и heap суть синонимы и обозначают динамически выделяемую память. Важно просто различать память полученную от new
и от malloc()
.
И даже более того: если ваше приложение использует динамически загружаемые модули (напр. dll), то может статься, что память, выделенную в одном модуле (напр. при помощи new
) нельзя освобождать в другом модуле (при помощи delete
), т.к. модули слинкованы с разными версиями функций, использующими разные списки свободной памяти. Такое поведение, очевидно, запрещено стандартом, но в реальной жизни встречается и приводит к чрезвычайно трудноуловимым ошибкам, безмолвно превращая корректный код в некорректный.
auto_ptr
. Часть 1auto_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> >
уже грубейшая ошибка.
auto_ptr
. Часть 1// Наконец-то верно! auto_ptr<String> f() { auto_ptr<String> result=new String;
Ох уж мне эти учебные примерчики! Во-первых, при работе со стандартным вводом/выводом C++ о строгой гарантии можно сразу же забыть, т.к. в поток может быть выведена только часть информации, и если после этого возникнет исключение, то откат частично выведенной информации будет никак не возможен.
А во-вторых, и это уже просто смешно, автор забыл, что auto_ptr
имеет explicit
конструктор, поэтому приведенная им инициализация вызовет ошибку компиляции. Нужно сразу вызывать конструктор: auto_ptr<String> result(new String);
.
ValuePtr
разрешено и имеет семантику создания копии принадлежащего ValuePtr
объекта Y
с использованием виртуального метода Y::Clone()
при наличии такового или конструктора копирования Y
в противном случае.
Я бы вам рекомендовал ни в коем случае не придерживаться подобного рода тактики в реальном коде. Дело в том, что копирование объектов имея на руках всего лишь указатель на (возможно базовый) класс -- штука чрезвычайно тонкая. Автоматизация подобного рода операции -- это активный поиск неприятностей, т.к. код ValuePtr
может обнаружить, что в классе нет "правильно оформленной" Y* Y::Clone()
(в то время как есть sh_ptr<Y> Y::clone()
) и попытаться самостоятельно создать объект класса Y
(в то время как Y
-- абстрактный класс).
Правильное решение -- это требование к пользователю самостоятельно предоставить функцию клонирования, дав ему удобные механизмы для быстрой ее реализации. При таком подходе пользователь получит четкое сообщение об ошибке компиляции, если что-то недоглядит, в то время как "автоматизация" безмолвно сгенерирует некорректный код.
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="изменяем"; } }
Лучше бы автору этого не делать, т.к. говорить мимоходом об отдельных аспектах многопоточного программирования -- это все равно что пытаться объяснить как быстро переключиться на полужирный шрифт человеку, который ни разу в жизни не включал компьтера. И даже хуже того, в тексте сквозит полное непонимание сути многопоточного программирования, а код пестрит такими серьезными ошибками, что даже не хочется комментировать.
По сути, многопоточное программирование возникло из необходимости эффективно исполнять отдельные части кода на нескольких процессорах параллельно. Тому, кто это понимает, никогда не придет в голову запустить в разных потоках код, который неявно синхронизируется где-то за кулисами, т.к. от введения синхронизации параллельность работы просто убивается и многопоточное приложение начинает работать медленнее своего однопоточного аналога.
Как вы уже наверное догадались, в объектно-ориентированных языках роль указателей на функции гораздо лучше выполняют интерфейсы, которые позволяют не только реализовать "указатель на действие", но и сохранять в объекте-реализации всю необходимую информацию между вызовами:
// интерфейс -- состояние конечного автомата 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
).
Весьма забавная задачка, в которой автор не только не приводит случаев полезности вложенных функций (хотя, в принципе, мог бы кивнуть в сторону не менее бесполезных стандартных алгоритмов типа find_if
), но и приводит просто ужасный код без проблеска здравого смысла.
А для тех, кто хочет узнать, почему в C/C++ нет вложенных функций объясняю:
На самом деле, сответствующий раздел стандарта имеет номер 14.3.1p2 и написано там следующее: "Локальный тип, тип без компоновки (no linkage), неименованный тип или тип, составленный из любого из этих типов, не может использоваться в качестве аргумента для параметра шаблона".
Т.к. дорогие переводчики в который раз испоганили все что можно и не можно, приведу нормальный перевод нескольких абзацев.
В этом случае мы получим:
class U : T { /* ... */ }; U& U::operator=(const U& other) { T::operator=(other); // ... а здесь выполняем присваивание членов U ... // ... оппаньки, а ведь мы уже не U! return *this; // те же оппаньки }Как уже было отмечено, вызов
T::operator=()
безмолвно портит весь последующий код (и присваивание членов U
и return
). Проявляется же это, чаще всего, в виде таинственной и трудной для отладки ошибки исполнения, т.к. деструктор U
не уничтожает свои данные-члены.
Для устранения данной проблемы мы можем попробовать несколько способов:
U
должен вызывать "this->T::~T();
" с последующим размещающим new
для базового подобъекта T
? Хорошо, это гарантирует нам, что будет изменен только лишь базовый подобъект T
(вместо срезки всего производного объекта и превращения его в T
) [мое примечание: на самом деле это ничуть не лучше вызова T::operator=()
, который тоже воздействует только на подобъект T
, однако портит значение указателя на таблицу виртуальных функций, который хранится в части T
, но используется обоими классами]. Оказывается, что и в этом случае есть ловушки, но пока не будем отказываться от этой идеи.
U
следовать идиоме уничтожения на месте (всего объекта U
) и размещающего пересоздания? Это более хорошая альтернатива, но она иллюстрирует другую слабость идиомы. Если ее использует один из классов, то данную идиому должны использовать и все классы, производные от него. (Это плохо по многим причинам, например из-за того, что оператор присваивания, генерируемый компилятором по умолчанию, ей не следует и большинство программистов при наследовании без добавления данных-членов и не подумают о предоставлении собственной версии оператора присваивания, считая, что оператор присваивания по умолчанию вполне подойдет.)
Чрезвычайно глубокое и тонкое замечание. Огорчает лишь то, что автор совершенно забыл о том, что "канонический вид строго безопасного оператора присваивания"
T& operator=(const T& other) { T temp(other); Swap(temp); return *this; }еще более жестоко "извращает смысл конструирования и деструкции", т.к. время жизни не только начинается и заканчивается, но и в процессе присваивания задействовано уже целых три объекта с пересекающимся временем жизни! Увы, налицо острая некогерентность мышления.
const
Думаю, что данная рекомендация внимания не заслуживает, т.к. аргумент, что это поможет дураку не написать что-то вроде poly.GetPoint(i)=Point(2,2);
нельзя назвать серьезным. Законы Мерфи утверждают, что
pb=(B*)&c1;
reinterpret_cast
:pb=reinterpret_cast<B*>(&c1);
Просто класс! Правильный совет: убедитесь, что классы B
и C
никак не связаны и никогда так не делайте!
Никакая часть данного материала не может быть использована в коммерческих целях без письменного разрешения автора.