Сообщений 4 Оценка 376 [+0/-2] Оценить |
Немного сложнее Заключение Ссылки |
Некоторое время тому назад я написал простой шаблон класса-примеси. А неделю спустя обнаружил в нем небольшой изъян. Хотя решение нашлось практически мгновенно, я все-таки решил разобраться в проблеме поглубже. Это стоило сделать хотя бы потому, что проблема касалась фундаментальных свойств языка C++.
Вот проблемный код:
template<class T> struct Mixin : T { ~Mixin(); }; |
Я догадываюсь, о чем вы подумали: «Да этот класс выглядит, как пример из книги по C++». Вы наверняка встречались с подобным кодом. Эти чувства, скорей всего, основаны на бесспорном знании простых конструкций C++. Тем не менее, несмотря на простоту, в этом коде есть одна маленькая проблема.
Почему этот код не вызывает подозрений на первый взгляд? Если бы это был обычный класс, можно было бы просто скомпилировать его и увидеть, что все замечательно. Но это не пройдет в случае с шаблонами. Написание кода – это только половина работы. Другая половина – конкретизация (этот термин, возможно, не вполне удачен в качестве перевода для instantiating, но лучшего найти пока не удается. - прим.ред.) шаблона. Это придется делать пользователю класса, если только вы не протестируете свой шаблон сами, параметризуя его всеми возможными аргументами.
Здесь нужно другое мышление. Когда имеешь дело с шаблонами, нужно представлять, как они будут компилироваться, будучи параметризованы различными аргументами. Вы можете заявить: «Ну и в чем проблема? Я могу написать тесты и конкретизировать шаблоны в них». Да, можете. Но сперва нужно найти правильные классы для конкретизации. В качестве примера: можете ли вы найти такую реализацию Mixin<X>, которая нарушит код, приведенный выше?
Не утруждайте себя. У меня уже есть ответ. Вот он:
struct X { virtual ~X() throw(); }; |
Итак, нужный класс найден, можно попробовать скомпилировать его. Мой компилятор (g++ 3.2.2) при этом выдает следующее:
1.cpp: In instantiation of 'Mixin<X>': 1.cpp:12: instantiated from here 1.cpp:3: looser throw specifier for 'void Mixin<T>::Mixin() [with T = X]' 1.cpp:7: overriding 'virtual X::~X() throw ()' |
Что же это означает? Смотрим стандарт C++, параграф 15.4, абзац 3:
Если виртуальная функция имеет спецификацию исключения, все объявления, включая определение, любой функции, переопределяющей эту функцию в любом из классов-наследников, должны выбрасывать только те исключения, которые разрешены спецификацией исключений виртуальной функции базового класса.
В деструкторе базового класса X не разрешены никакие исключения. Поэтому их не должно быть и в классе-наследнике Mixin<X>:
template<class T> struct Mixin : T { ~Mixin() throw(); }; |
Итак, мы быстро нашли решение проблемы. Имеет ли оно изъяны? Может ли оно помешать параметризации данного шаблона другими аргументами? Например, что произойдет, если деструктор класса T неожиданно выкинет исключение? Mixin<X> имеет пустой список разрешенных исключений, следовательно, будет вызвана функция std::unexpected. Она, в свою очередь, вызовет std::terminate и выполнение программы будет прервано. Определенно, это совсем не то поведение, которое ожидает пользователь.
К счастью, гуру C++ рекомендуют не выкидывать исключений в деструкторах вообще. Поэтому будет достаточно указать в документации, что деструктор класса T должен удовлетворять требованию Nothrow.
Вроде бы проблема решена. И действительно, если вы охотник за ошибками, только-только разобравшийся с кодом, подобным приведенному выше, можете закончить чтение. Я же продолжу анализ.
Мне не нравится в деструкторе с пустой спецификацией исключений то, что компилятор может поместить его код в блок try-catch. Это предохраняет код от «утечек исключений». Этот блок можно исключить только в том случае, если тело деструктора доступно и компилятор может сделать вывод, что деструктор никогда не выдаст исключение. В противном случае блоки try-catch приводят к раздуванию кода и замедлению его выполнения.
Другой недостаток вышеприведенного кода заметил Фил Басс, когда просматривал мою статью. Его наблюдение относится скорее к дизайну, чем к деталям реализации. Фил заметил, что если шаблон Mixin является частью библиотеки широкого назначения, было бы здорово, если бы он следовал правилам генерации исключений, определяемым проектом, в котором библиотека используется.
Существуют две стратегии использования спецификаций исключений в деструкторах:
Наверное, первая стратегия используется гораздо шире, чем вторая. Но все-таки в C++-проектах используются обе. Например, стандартная библиотека C++ использует оба подхода.
Не стоит и говорить, что деструктор Mixin<T>, нейтральный к стратегии спецификации исключений, намного предпочтительней заставляющего нас выбирать ту или иную стратегию.
Я советую прервать на время чтение и попытаться найти лучшее из всех решение. Решение, свободное от ограничений первой версии Mixin и не диктующее конкретную стратегию.
Даже при столь небольшом выборе вариантов решение может показаться неожиданным: не определять деструктор вообще, то есть положиться на компилятор.
template<class T> struct Mixin : T { }; |
Почему это решение лучше? Чтобы объяснить это, снова сошлюсь на стандарт C++, параграф 15.4, абзац 13. Кроме объяснения приведенного решения, там содержится еще и пример с множественным наследованием, который будет рассмотрен позже. В моей вольной интерпретации: неявно определенный деструктор «наследует» спецификацию исключений от деструктора базового класса. Таким образом, какую бы спецификацию не задавал деструктор класса T, такую же точно будет иметь и деструктор шаблона Mixin<T>. Замечательно, это именно то, что нам нужно!
Вы можете спросить: как добиться того, чтобы явные деструкторы были не нужны в реальных шаблонах? Я рекомендую использовать обертки в стиле RAII, «умные» указатели, строки и контейнеры стандартной библиотеки C++ везде, где только можно. Это сводит потребность явно задавать деструкторы к исключительным случаям.
Итак, настало время решить проблему, с которой я столкнулся. Она очень похожа на первоначальный пример, с одним отличием – Mixin имеет дополнительный базовый класс.
struct Base { // ... }; template<class T> struct Mixin : Base, T { // ... }; |
Ясно, что всегда можно использовать деструктор с пустой спецификацией в Mixin. Я хотел бы, чтобы вы проанализировали случай с неявным деструктором. Помните только, что, с одной стороны, неявный деструктор наследует спецификацию исключений от всех баз, а с другой стороны, если какой-либо из базовых деструкторов является виртуальным, ~Mixin() не может иметь менее строгую спецификацию исключений.
Первый случай: класс Base имеет невиртуальный деструктор. Анализ показывает, что деструктор класса Base должен иметь пустой список исключений, чтобы деструктор ~Mixin() можно было определить неявно.
struct Base { ~Base() throw(); }; template<class T> struct Mixin : Base, T { }; |
Хотя это решение навязывает стратегию выбора спецификаций исключений базового класса, оно все равно не лишено интереса, так как получившийся шаблон Mixin нейтрален к используемой пользователем стратегии спецификаций исключений.
Второй случай не имеет решения. Если деструктор Base виртуальный, мы всегда сможем найти такой тип T, который нарушит компиляцию, несмотря на спецификацию исключений деструктора ~Base().
Это как раз и был мой случай. Я мог бы перенести виртуальный деструктор из базового класса в другой класс, отвечающий за полиморфное клонирование и удаление (можно сказать, управление памятью). Этот класс мог бы лучше вписаться в концепцию единой ответственности, но я решил использовать другое решение:
struct Base { virtual ~Base(); }; template<class T> struct Mixin : Base, T { ~Mixin() throw(); }; |
В заключение я хотел бы сделать два вывода. Во-первых, подвести итог проделанной работе.
Подмешиваемые классы часто встречаются в библиотеках общего назначения или в таких библиотеках, которые не делают никаких предположений о проектах, их использующих. Следование правилам и стратегиям проекта очень важно, даже если множество проектов, в которых будет использоваться библиотека, неизвестно ее автору. В этой статье я показал, как решить частную проблему с учетом возможного дальнейшего использования вашего кода.
Второй вывод, скорее, философский. Даже если вам когда-нибудь встретится код еще проще, чем обсуждавшийся в данной статье, не поленитесь проанализировать его. Возьму на себя смелость заявить, что в языке C++ не бывает мелочей. В мире C++ все важно. Если у вас есть интересные наблюдения, касающиеся возможностей языка или их побочных эффектов, попробуйте поиграть с ними. Множество трюков C++ и современных методик программирования были найдены именно таким образом. Дерзайте! И вместе мы улучшим язык.
[ISO] ISO/IEC 14882
Сообщений 4 Оценка 376 [+0/-2] Оценить |