|
||||
|
Шаблоны и обобщенность
Аналогично: место для вашего введения. В этом разделе мы считаем наиболее значимой рекомендацию 64 — "Разумно сочетайте статический и динамический полиморфизм". 64. Разумно сочетайте статический и динамический полиморфизмРезюмеСтатический и динамический полиморфизм дополняют друг друга. Следует ясно представлять себе их преимущества и недостатки, чтобы использовать каждый из них там, где он дает наилучшие результаты, и сочетать их так, чтобы получить лучшее из обоих миров. ОбсуждениеДинамический полиморфизм предстает перед нами в форме классов с виртуальными функциями и объектов, работа с которыми осуществляется косвенно — через указатели или ссылки. Статический полиморфизм включает шаблоны классов и функций. Полиморфизм означает, что данное значение может иметь несколько типов, а данная функция может принимать аргументы типов, отличающихся от точных типов ее параметров. "Полиморфизм представляет собой способ получить немного свободы динамической проверки типов, не теряя преимуществ статической проверки" — [Webber03]. Сила полиморфизма состоит в том, что один и тот же фрагмент кода может работать с разными типами, даже с теми, которые не были известны в момент написания этого кода. Такая "применимость задним числом" является краеугольным камнем полиморфизма, поскольку существенно увеличивает пригодность и возможность повторного использования кода (см. рекомендацию 37). (В противоположность этому мономорфный код работает только со строго конкретными типами, теми, для работы с которыми он изначально создавался.) Динамический полиморфизм позволяет значению иметь несколько типов посредством открытого наследования. Например, Derived* pможно рассматривать как указатель не только на Derived, но и на объект любого типа Base, который прямо или косвенно является базовым для Derived(свойство категоризации). Динамический полиморфизм известен также как включающий полиморфизм, поскольку множество, моделируемое Base, включает специализации, моделируемые Derived. Благодаря своим характеристикам динамический полиморфизм в С++ наилучшим образом подходит для решения следующих задач. • Единообразная работа, основанная на отношении надмножество/подмножество. Работа с различными классами, удовлетворяющими отношению надмножество/подмножество (базовый/производный), может выполняться единообразно. Функция, работающая с объектом Employee(Служащий), будет работать и с объектами Secretary(Секретарь). • Статическая проверка типов. В С++ все типы проверяются статически. • Динамическое связывание и раздельная компиляция. Код, который использует иерархию классов, может компилироваться отдельно от этой иерархии. Это становится возможным благодаря косвенности, обеспечиваемой указателями (как на объекты, так и на функции). • Бинарная согласованность. Модули могут компоноваться как статически, так и динамически, до тех пор, пока схемы виртуальных таблиц подчиняются одним и тем же правилам. Статический полиморфизм посредством шаблонов также позволяет значению иметь несколько типов. Внутри шаблона template<class T> void f(T t) { /* ... */ } tможет иметь любой тип, который можно подставить в fдля получения компилируемого кода. Это называется "неявным интерфейсом" в противоположность явному интерфейсу базового класса. Таким образом достигается та же цель полиморфизма — написание кода, который работает с разными типами — но совершенно иным путем. Статический полиморфизм наилучшим образом подходит для решения следующих задач. • Единообразная работа, основанная на синтаксическом и семантическом интерфейсе. Работа с типами, которые подчиняются синтаксическому и семантическому интерфейсу, может выполняться единообразно. Интерфейсы в данном случае представляют синтаксическую сущность и не основаны на сигнатурах, так что допустима подстановка любого типа, который удовлетворяет данному синтаксису. Например, пусть дана инструкция int i = p->f(5);. Если p— указатель на класс Base, эта инструкция вызывает определенную функцию интерфейса, вероятно, virtual int f(int). Но если pимеет обобщенный тип, то этот вызов может быть связан со множеством различных вещей, включая, например, вызов перегруженного оператора operator->, который возвращает тип, в котором определена функция X f(double), где X— тип, который может быть преобразован в int. • Статическая проверка типов. Все типы проверяются статически. • Статическое связывание (мешает раздельной компиляции). Все типы связываются статически. • Эффективность. Вычисления во время компиляции и статическое связывание позволяют достичь оптимизации и эффективности, недоступных при динамическом связывании. Определите ваши приоритеты и используйте каждый вид полиморфизма там, где проявляются его сильные стороны. Следует сочетать статический и динамический полиморфизм для того, чтобы получить преимущества обоих видов полиморфизма, а не для того, чтобы комбинировать их недостатки. • Статика помогает динамике. Используйте статический полиморфизм для реализации динамически полиморфных интерфейсов. Например, у вас может быть абстрактный базовый класс Command, и вы определяете различные реализации в виде шаблона template</* ... */> class ConcreteCommand: public Command В качестве примеров можно привести реализации шаблонов проектирования Commandи Visitor(см. [Alexandrescu01] и [Sutter04]). • Динамика помогает статике. Обобщенный, удобный, статически связываемый интерфейс может использовать внутреннюю динамическую диспетчеризацию, что позволяет обеспечить одинаковую схему размещения объектов. Хорошими примерами могут служить реализации размеченных объединений (см. [Alexandrescu02b] и [Boost]) и параметр Deleterу tr1::shared_ptr(см. [C++TR104]). • Прочие сочетания. Плохим является сочетание, при котором комбинируются слабые стороны обоих видов полиморфизма и результат получается хуже, чем при их отдельном использовании. Правильное сочетание должно комбинировать лучшее от обоих видов полиморфизма. Например, не помещайте виртуальные функции в шаблон класса, если только вы не хотите, чтобы каждый раз инстанцировались все виртуальные функции (в противоположность невиртуальным функциям шаблонных типов). В результате вы можете получить астрономический размер кода и чрезмерно ограничить ваш обобщенный тип, инстанцируя функциональность, которая никогда не используется. Ссылки[Alexandrescu01] §10 • [Alexandrescu02b] • [C++TR104] • [Gamma95] • [Musser01] §1.2-3, §17 • [Stroustrup00] §24.4.1 • [Sutter00] §3 • [Sutter02] §1 • [Sutter04] §17, §35 • [Vandevoorde03] §14 • [Webber03] §8.6 65. Выполняйте настройку явно и преднамеренноРезюмеПри разработке шаблона точки настройки должны быть написаны корректно, с особой тщательностью, а также ясно прокомментированы. При использовании шаблона необходимо четко знать, как именно следует настроить шаблон для работы с вашим типом, и выполнить соответствующие действия. ОбсуждениеРаспространенная ловушка при написании библиотек шаблонов заключается в наличии непреднамеренных точек настройки, т.е. точек в вашем шаблоне, где может выполняться поиск пользовательского кода и его использование, но при написании такие действия вами не подразумевались. Попасть в такую ловушку очень легко — достаточно просто вызвать другую функцию или оператор обычным путем (без полной его квалификации), и если окажется, что один из его аргументов имеет тип параметра шаблона (или связанный с ним), то будет начат поиск такого кода, зависящий от аргумента. Примеров тому множество; в частности, см. рекомендацию 58. Поэтому лучше использовать такие точки преднамеренно. Следует знать три основных пути обеспечения точек настройки в шаблоне, решить, какой именно способ вы хотите использовать в данном месте шаблона, и корректно его закодировать. Затем проверьте, не осталось ли в вашем коде случайных точек настройки там, где вы не предполагали их наличие. Первый способ создания точки настройки — обычный "неявный интерфейс" (см. рекомендацию 64), когда ваш шаблон просто рассчитывает на то, что тип имеет соответствующий член с данным именем: // Вариант 1. Создание точки настройки путем требования от // типа T "foo-совместимости", т.е. наличия функции-члена с // данным именем, сигнатурой и семантикой template<typename T> void Sample1(T t) { t.foo(); // foo - точка настройки typename T::value_type x; // Еще один пример: создание } // точки настройки для поиска // типа (обычно создается посредством typedef) Для реализации первого варианта автор Sample1должен выполнить следующие действия. • Вызвать функцию как член. Просто используйте естественный синтаксис вызова функции-члена. • Документировать точку настройки. Тип должен обеспечить доступную функцию-член foo, которая может быть вызвана с данными аргументами (в данном случае — без аргументов). Второй вариант представляет собой использование метода "неявного интерфейса", но с функциями, не являющимися членами, поиск которых выполняется с использованием ADL[3](т.е. ожидается, что данная функция находится в пространстве имен типа, для которого выполняется инстанцирование шаблона). Именно эта ситуация и явилась основной побудительной причиной для введения ADL (см. рекомендацию 57). Ваш шаблон рассчитывает на то, что для используемого типа имеется подходящая функция с заданным именем: // Вариант 2: Создание точки настройки путем требования от // типа T "fоо-совместимости", т.е. наличия функции, не // являющейся членом с данным именем, сигнатурой и // семантикой, поиск которой выполняется посредством ADL. // (Это единственный вариант, при котором не требуется поиск // самого типа T.) template<typename T> void Samplе2(T t) { foo(t); // foo - точка настройки cout << t; // Еще один пример - operator<< с записью в } // виде оператора представляет собой такую же // точку настройки Для реализации варианта 2 автор Samplе2должен выполнить следующие действия. • Вызвать функцию с использованием неквалифицированного имени (включая использование естественного синтаксиса в случае операторов) и убедиться, что шаблон не имеет функции-члена с тем же именем. В случае шаблонов очень важно, чтобы вызов функции был не квалифицированным (например, не следует писать SomeNamespace::foo(t)) и чтобы у шаблона не было функции-члена с тем же именем, поскольку в обоих этих случаях поиск, зависящий от аргумента, выполняться не будет, что предотвратит поиск имени в пространстве имен, в котором находится тип T. • Документировать точку настройки. Тип должен обеспечить наличие функции, не являющейся членом, которая может быть вызвана с данными аргументами. Варианты 1 и 2 имеют одинаковые преимущества и применимость: пользователь может один раз написать соответствующую функцию настройки для своего типа и разместить ее там, где ее смогут найти и шаблоны других библиотек. Тем самым пользователь избегает необходимости писать множество мелких адаптеров для каждой библиотеки отдельно. Недостаток же заключается в том, что соответствующая семантика должна быть достаточно широко применима и иметь смысл для всех такого рода потенциальных применений (заметим, что в частности в эту категорию попадают операторы, что является еще одной причиной для рекомендации 26). Третий вариант заключается в использовании специализации, когда ваш шаблон полагается на то, что пользовательский тип специализирует (при необходимости) некоторый иной предоставленный вами шаблон класса. // Вариант 3: Создание точки настройки путем требования от // типа T "foo-совместимости" путем специализации шаблона // SampleTraits<> с предоставлением (обычно статической) // функции с данным именем, сигнатурой и семантикой. template<typename T> void Samplе3(T t) { S3Traits<T>::foo(t); // S3Traits<>::foo - // точка настройки typename S3Traits<T>::value_type x; // Другой пример - } // точка настройки для поиска типа (обычно // создается посредством typedef) В этом варианте пользователь пишет адаптер, который гарантирует изолированность кода настройки для данной библиотеки в пределах этой библиотеки. Соответствующий недостаток заключается в том, что это может оказаться слишком громоздким решением; если несколько библиотек шаблонов требуют одну и ту же общую функциональность, пользователь должен будет писать несколько адаптеров, по одному для каждой библиотеки. Для реализации этой версии автор Samplе3должен выполнить следующие действия. • Предоставить шаблон класса по умолчанию в собственном пространстве имен шаблона. Не используйте шаблоны функций, которые нельзя частично специализировать и которые приводят к перегрузкам и зависимостям от порядка (см. также рекомендацию 66). • Документировать точку настройки. Пользователь должен специализировать S3Traitsдля своего собственного типа в пространстве имен библиотеки шаблонов, и документировать все члены S3Traits(например, foo) и их семантику. При использовании любого из перечисленных вариантов следует также четко документировать семантику, требуемую от foo, в особенности все существенные действия (постусловия), которые должна гарантировать эта функция, и семантику сбоев (что именно происходит при сбое и каким образом должно осуществляться оповещение об ошибках). Если точка настройки должна действовать и для встроенных типов, используйте варианты 2 и 3. Варианты 1 и 2 следует предпочесть для тех общих операций, которые являются предоставляемыми типом сервисами. Для принятия данного решения попробуйте ответить на следующие вопросы: могут ли другие библиотеки шаблонов использовать данную возможность? является ли рассматриваемая семантика приемлемой для данного имени в общем случае? Если вы положительно ответили на эти вопросы, то, вероятно, вам действительно следует предпочесть один из этих вариантов. Вариант 3 лучше использовать для менее общих операций, смысл которых может варьироваться. В таком случае в другом пространстве имен без каких-либо коллизий вы сможете придать тому же имени иной смысл. Шаблон, в котором имеется несколько точек настройки, для каждой из них может выбрать свою стратегию, в наибольшей мере приемлемую в данном месте. Главное, что вы должны осознанно, с пониманием выбирать стратегию для каждой точки настройки, документировать требования к настройке (включая ожидаемые постусловия и семантику ошибок) и корректно реализовать выбранную вами стратегию. Для того чтобы избежать непреднамеренных точек настройки, следует придерживаться следующих правил. • Размещайте все используемые вашим шаблоном вспомогательные функции в их собственном вложенном пространстве имен, и вызывайте их посредством полностью квалифицированных имен для запрета ADL. Если вы вызываете вашу вспомогательную функцию и передаете ей объект типа параметра шаблона, и этот вызов не должен быть точкой настройки (т.е. вы всегда намерены вызывать вашу вспомогательную функцию, а не некоторую иную), то лучше поместить эту вспомогательную функцию во вложенное пространство имен и явно запретить ADL, полностью квалифицировав имя вызываемой функции или взяв его в скобки: template<typename T> void Samplе4(T t) { S4Helpers::bar(t); // Запрет ADL: bar не является // точкой настройки (bar)(t); // Альтернативный способ } • Избегайте зависимости от зависимых имен. Говоря неформально, зависимое имя — это имя, которое каким-то образом упоминает параметр шаблона. Многие компиляторы не поддерживают "двухфазный поиск" для зависимых имен из стандарта С++, а это означает, что код шаблона, использующий зависимые имена, будет вести себя по-разному на разных компиляторах, если только не принять меры для полной определенности при использовании зависимых имен. В частности, особого внимания требует наличие зависимых базовых классов, когда шаблон класса наследуется от одного из параметров этого шаблона (например, Tв случае template<typename T>class С:T{};) или от типа, который построен с использованием одного из параметров шаблона (например, X<T>в случае template<typename T>class C:X<T>{};). Коротко говоря, при обращении к любому члену зависимого базового класса необходимо всегда явно квалифицировать имя с использованием имени базового класса или при помощи this->. Этот способ можно рассматривать просто как некую магию, которая заставляет все компиляторы делать именно то, что вы от них хотите. template<typename T> class С : X<T> { typename X<T>::SomeType s; // Использование вложенного // типа (или синонима // typedef) из базового // класса public: void f() { X<T>::baz(); // вызов функции-члена // базового класса this->baz(); // Альтернативный способ } }; Стандартная библиотека С++ в основном отдает предпочтение варианту 2 (например, ostream_iteratorищет оператор operator<<, a accumulateищет оператор operator+в пространстве имен вашего типа). В некоторых местах стандартная библиотека использует также вариант 3 (например, iterator_traits, char_traits) в основном потому, что эти классы свойств должны быть специализируемы для встроенных типов. Заметим, что, к сожалению, стандартная библиотека С++ не всегда четко определяет точки настройки некоторых алгоритмов. Например, она ясно говорит о том, что трехпараметрическая версия accumulateдолжна вызывать пользовательский оператор operator+с использованием второго варианта. Однако она не говорит, должен ли алгоритм sortвызывать пользовательскую функцию swap(обеспечивая таким образом преднамеренную точку настройки с использованием варианта 2), может ли он использовать пользовательскую функцию swap, и вызывает ли он функцию swapвообще; на сегодняшний день некоторые реализации sortиспользуют пользовательскую функцию swap, в то время как другие реализации этого не делают. Важность рассматриваемой рекомендации была осознана совсем недавно, и сейчас комитет по стандартизации исправляет ситуацию, устраняя такие нечеткости из стандарта. Не повторяйте такие ошибки. (См. также рекомендацию 66.) Ссылки [Stroustrup00] §8.2, §10.3.2, §11.2.4 • [Sutter00] §31-34 • [Sutter04d] 66. Не специализируйте шаблоны функцийРезюмеПри расширении некоторого шаблона функции (включая std::swap) избегайте попыток специализации шаблона. Вместо этого используйте перегрузку шаблона функции, которую следует поместить в пространство имен типа(ов), для которых разработана данная перегрузка (см. рекомендацию 57). При написании собственного шаблона функции также избегайте его специализации. Обсуждение Шаблоны функций вполне можно перегружать. Разрешение перегрузки рассматривает все первичные шаблоны и работает именно так, как вы и ожидаете, исходя из вашего опыта работы с перегрузкой обычных функций С++: просматриваются все видимые шаблоны и выбирается шаблон с наилучшим соответствием. К сожалению, в случае специализации шаблона функции все оказывается несколько сложнее по двум основным причинам. • Специализировать шаблоны функций можно только полностью, но не частично. Код, который выглядит как частичная специализация, на самом деле представляет собой перегрузку. • Специализации шаблона функции никогда не участвуют в перегрузке. Таким образом, любая написанная вами специализация никак не повлияет на результат разрешения перегрузки и выбор используемого шаблона. Это противоречит интуитивно ожидаемому поведению разрешения перегрузки. Но, в конце концов, если вы напишете нешаблонную функцию с идентичной сигнатурой вместо специализации шаблона функции, то при разрешении перегрузки будет выбрана именно нешаблонная функция, как имеющая преимущество перед шаблоном. Если вы пишете шаблон функции, то лучше писать его как единый шаблон, который никогда не будет специализирован или перегружен, и реализовывать шаблон функции через шаблон класса. Это и есть тот пресловутый дополнительный уровень косвенности, который позволяет обойти ограничения и миновать "темные углы" шаблонов функций. В этом случае программист, использующий ваш шаблон, сможет частично специализировать шаблон класса. Тем самым решается как проблема по поводу того, что шаблон функции не может быть частично специализирован, так и по поводу того, что специализации шаблона функции не участвуют в перегрузке. Если вы работаете с каким-то иным старым шаблоном функции, в котором не использована описанная методика (т.е. с шаблоном функции, не реализованном посредством шаблона класса), и хотите написать собственную версию для частного случая, которая должна принимать участие в перегрузке, — делайте ее не специализацией, а обычной нешаблонной функцией (см. также рекомендации 57 и 58). Примеры Пример. std::swap. Базовый шаблон swapобменивает два значения аи bпутем создания копии tempзначения а, и присваиваний a = bи b = temp. Каким образом расширить данный шаблон для ваших собственных типов? Пусть, например, у вас есть ваш собственный тип Widgetв вашем пространстве имен N: namespace N { class Widget {/*...*/}; } Предположим, что имеется более эффективный путь обмена двух объектов Widget. Что вы должны сделать для того, чтобы он использовался стандартной библиотекой, — перегрузить swap(в том же пространстве имен, где находится Widget; см. рекомендацию 57) или непосредственно специализировать std::swap? Стандарт в данном случае невразумителен, и на практике используются разные методы (см. рекомендацию 65). Сегодня ряд реализаций корректно решают этот вопрос, предоставляя перегруженную функцию в том же пространстве имен, где находится Widget. Для представленного выше нешаблонного класса Widgetэто выглядит следующим образом: namespace N { void swap(Widget&, Widget&); } Заметим, что если Widgetявляется шаблоном namespace N { template<typename T> class Widget { /* ... */ }; } то специализация std::swapпопросту невозможна, так как частичной специализации шаблона функции не существует. Лучшее, что вы можете сделать, — это добавить перегрузку функции namespace ??? { template<typename T> void swap(Widget<T>&, Widget<T>&); } Это проблематичное решение, поскольку если вы помещаете эту функцию в пространство имен, в котором находится Widget, то многие реализации просто не в состоянии найти ее, но при этом стандарт запрещает располагать данную функцию в пространстве имен std. Эта проблема никогда бы не возникла, если бы стандарт либо указывал, что перегрузки надо искать и в пространстве имен типа шаблона, либо позволял помещать перегружаемые функции в пространство имен std, или (возвращаясь к основному вопросу данной рекомендации) прямо указывал, что swapдолжна реализовываться с использованием шаблона класса, который может быть частично специализирован. Ссылки [Austern99] §A.1.4 • [Sutter04] §7 • [Vandevoorde03] §12 67. Пишите максимально обобщенный кодРезюмеИспользуйте для реализации функциональности наиболее обобщенные и абстрактные средства. ОбсуждениеКогда вы пишете тот или иной код, используйте наиболее абстрактные средства, позволяющие решить поставленную задачу. Всегда думайте над тем, какие операции накладывают меньшее количество требований к интерфейсам, с которыми они работают. Такая привычка сделает ваш код более обобщенным, а следовательно, в большей степени повторно используемым и более приспособленным ко внесению изменений в его окружение. И напротив, код, неоправданно привязанный к деталям, оказывается чрезмерно "жестким" и неспособным к повторному использованию. • Используйте для сравнения итераторов !=вместо <. Оператор !=более общий и применим к большему классу объектов; оператор <требует упорядочения и может быть реализован только итераторами произвольного доступа. При использовании оператора !=ваш код проще переносится для работы с другими типами итераторов, такими как одно- и двунаправленные итераторы. • Лучше использовать итераторы, а не индексы. Многие контейнеры не поддерживают индексный доступ; например, контейнер listне в состоянии эффективно реализовать его. Однако все контейнеры поддерживают итераторы. Таким образом, итераторы обеспечивают большую обобщенность кода, и при необходимости они могут использоваться совместно с индексным доступом. • Используйте empty()вместо size()==0. "Пуст/не пуст" — более примитивная концепция, чем "точный размер". Например, вы можете не знать размер потока, но всегда можете сказать о том, пуст он или нет; то же самое справедливо и для входных итераторов. Некоторые контейнеры, такие как list, реализуют emptyболее эффективно, чем size. • Используйте наивысший класс иерархии, предоставляющий необходимую вам функциональность. При программировании с использованием динамических полиморфных классов не следует делать код зависимым от ненужных вам деталей и привязываться к определенным производным классам. • Будьте корректны при использовании const(см. рекомендацию 15). Передача параметров const&накладывает меньше ограничений на вызывающий код, поскольку const&охватывает как константные, так и неконстантные объекты. Исключения В некоторых случаях применение индексов вместо итераторов позволяет компилятору лучше оптимизировать код. Однако перед тем как решиться на такой шаг, убедитесь, что вы действительно в нем нуждаетесь и что ваш компилятор действительно при этом лучше оптимизирует ваш код (см. рекомендацию 8). Ссылки[Koenig97] §12.7, §17-18 • [Meyers01] §4 • [Stroustrup00] §13, §17.1.1 • [Sutter04] §1, §5, §34 Примечания:3 Поиск, зависящий от аргумента (см. стр. 118). — Прим. перев. |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|