Главная страница
Библиотека (скачать книги)
Скачать софт
Введение в программирование
Стандарты для C++
Уроки по C#
Уроки по Python
HTML
Веб-дизайн
Ассемблер в среде Windows
ActiveX
Javascript
Общее о Линукс
Линукс - подробно
Линукс - новое
Delphi
Паскаль для начинающих
Турбопаскаль
Новости
Партнеры
Наши предложения
Архив новостей







63. Используйте достаточно переносимые типы в интерфейсах модулей

Резюме

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

Обсуждение

Чем более широко распространяется ваша библиотека, тем меньше ваш контроль над средами программирования, используемыми вашими клиентами, и тем меньше множество типов, которые ваша библиотека может надежно использовать в своем внешнем интерфейсе. Взаимодействие между модулями включает обмен бинарными данными. Увы, C++ не определяет стандартные бинарные интерфейсы; широко распространенные библиотеки для взаимодействия со внешним миром могут полагаться на такие встроенные типы, как int и char. Даже один и тот же тип на одном и том же компиляторе может оказаться бинарно несовместимым при компиляции с разными опциями.

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

Требуется найти определенный компромисс между проблемами используемых типов, которые могут не быть корректно восприняты всеми клиентами, и проблемами использования низкого уровня абстракции. Абстракция важна; если некоторые клиенты понимают только низкоуровневые типы и вы ограничены в использовании этими типами, то, возможно, следует подумать о дополнительных операциях, работающих с высокоуровневыми типами. Рассмотрим функцию SummarizeFile, которая получает в качестве аргумента файл. Имеется три варианта действий — передать указатель char* на строку в стиле С с именем файла; передать string с именем файла и передать объект istream или пользовательский объект File. Каждый из этих вариантов представляет свой уровень компромисса.

  • Вариант 1. char*. Очевидно, что тип char* доступен наиболее широкому кругу клиентов. К сожалению, это также наиболее низкоуровневый вариант; в частности, он более проблематичен (например, вызывающий и вызываемый код должны явно решить, кто именно выделяет память для строки и кто ее освобождает), более подвержен ошибкам (например, файл может не существовать), и менее безопасен (например, может оказаться подвержен классической атаке, основанной на переполнении буфера).

  • Вариант 2. string. Тип string доступен меньшему кругу клиентов, ограниченному использованием C++ и компиляцией с использованием той же реализации стандартной библиотеки, того же компилятора и совместимых настроек компилятора. Взамен мы получаем менее проблематичный (надо меньше беспокоиться об управлении памятью; однако см. рекомендацию 60) и более безопасный код (например, тип string увеличивает при необходимости свой буфер и не так подвержен атакам на основе переполнения буфера). Но и этот вариант относительно низкоуровневый, а потому так же открытый для ошибок, как и предыдущий (например, указанный файл может и не существовать).

  • Вариант 3. istream или File. Если уж вы переходите к типам, являющимся классами, т.е. в любом случае требуется, чтобы клиент использовал язык программирования C++, причем тот же компилятор с теми же опциями компиляции, то воспользуйтесь преимуществами абстракции: класс istream (или пользовательский класс File, представляющий собой оболочку вокруг istream, позволяющую устранить зависимость от реализации стандартной библиотеки) повышает уровень абстракции и делает API существенно менее проблематичным. Функция получает объект типа File или соответствующий входной поток, она не должна заботиться об управлении памятью для строк, содержащих имя файла, и защищена от множества ошибок, которые вполне возможны при использовании первых двух вариантов. Остается только выполнить несколько проверок: файл должен быть открыт, а его содержимое иметь верный формат, но, в принципе, этим и ограничивается список неприятностей, которые могут произойти в данном варианте.

Даже если вы предпочтете воспользоваться во внешнем интерфейсе модуля низкоуровневой абстракцией, всегда используйте во внутренней реализации абстракции максимально высокого уровня и преобразуйте их в низкоуровневые абстракции на границах модуля. Например, если у вас имеются клиенты, не использующие C++, вы можете воспользоваться непрозрачным указателем void* или дескриптором типа int для работы с клиентом, но во внутренней реализации используйте высокоуровневые объекты. Преобразование между этими объектами и выбранными низкоуровневыми типами выполняйте только в интерфейсе модуля.

Примеры

Пример. Использование std::string в интерфейсе модуля. Пусть мы хотим, чтобы модуль предоставлял следующую функцию API:

std::string Translate( const std::string& );

Для библиотек, используемых внутри одной команды компании, это обычно неплохое решение. Но если вы планируете динамически компоновать данный модуль с вызывающим кодом, который использует иную реализацию std::string (например, иное размещение в памяти), то из-за такого несоответствия могут случиться разные странные и неприятные вещи.

Мы встречались с разработчиками, которые пытались использовать собственный класс-оболочку CustomString для объектов std::string, но в результате они сталкивались с той же проблемой, поскольку не имели полного контроля над процессом сборки всех клиентских приложений.

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

void Translate( const char* src, char* dest, size_t destsize );

Использование низкоуровневой абстракции более переносимо, но всегда добавляет сложности; здесь, например, как вызывающий, так и вызываемый код должны явно использовать обрезку строки, если размера буфера оказывается недостаточно. (Заметим, что данная версия использует буфер, выделяемый вызывающим кодом, для того чтобы избежать ловушки, связанной с выделением и освобождением памяти в разных модулях — см. рекомендацию 60.)

Шаблоны и обобщенность

Место для вашей цитаты.
— Бьярн Страуструп (Bjarne Stroustrup),
§13

Аналогично: место для вашего введения.

В этом разделе мы считаем наиболее значимой рекомендацию 64 — "Разумно сочетайте статический и динамический полиморфизм".

64. Разумно сочетайте статический и динамический полиморфизм

Резюме

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

Обсуждение

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

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

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

Динамический полиморфизм позволяет значению иметь несколько типов посредством открытого наследования. Например, Derived*p можно рассматривать как указатель не только на Derived, но и на объект любого типа Base, который прямо или косвенно является базовым для Derived (свойство категоризации). Динамический полиморфизм известен также как включающий полиморфизм, поскольку множество, моделируемое Base, включает специализации, моделируемые Derived.

Благодаря своим характеристикам динамический полиморфизм в С++ наилучшим образом подходит для решения следующих задач.

  • Единообразная работа, основанная на отношении надмножество/подмножество. Работа с различными классами, удовлетворяющими отношению надмножество/подмножество (базовый/производный), может выполняться единообразно. Функция, работающая с объектом Employee (Служащий), будет работать и с объектами Secretary (Секретарь).

  • Статическая проверка типов. В C++ все типы проверяются статически.

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

  • Бинарная согласованность. Модули могут компоноваться как статически, так и динамически, до тех пор, пока схемы виртуальных таблиц подчиняются одним и тем же правилам.

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

template<class T> void f( T t ){ /*...*/ }

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

Статический полиморфизм наилучшим образом подходит для решения следующих задач.

  • Единообразная работа, основанная на синтаксическом и семантическом интерфейсе. Работа с типами, которые подчиняются синтаксическому и семантическому интерфейсу, может выполняться единообразно. Интерфейсы в данном случае представляют синтаксическую сущность и не основаны на сигнатурах, так что допустима подстановка любого типа, который удовлетворяет данному синтаксису. Например, пусть дана инструкция int i = p->f(5);. Если р — указатель на класс Base, эта инструкция вызывает определенную функцию интерфейса, вероятно, virtual int f(int). Но если p имеет обобщенный тип, то этот вызов может быть связан со множеством различных вещей, включая, например, вызов перегруженного оператора operator->, который возвращает тип, в котором определена функция X f(double), где X — тип, который может быть преобразован в int.

  • Статическая проверка типов. Все типы проверяются статически.

  • Статическое связывание (мешает раздельной компиляции). Все типы связываются статически.

  • Эффективность. Вычисления во время компиляции и статическое связывание позволяют достичь оптимизации и эффективности, недоступных при динамическом связывании.

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

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

  • Статика помогает динамике. Используйте статический полиморфизм для реализации динамически полиморфных интерфейсов. Например, у вас может быть абстрактный базовый класс Command, и вы определяете различные реализации в виде шаблона template</* ... */> class ConcreteCommand: public Command.

    В качестве примеров можно привести реализации шаблонов проектирования Command и Visitor.

  • Динамика помогает статике. Обобщенный, удобный, статически связываемый интерфейс может использовать внутреннюю динамическую диспетчеризацию, что позволяет обеспечить одинаковую схему размещения объектов. Хорошими примерами могут служить реализации размеченных объединений.

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



 
 

Библиотека программиста. 2009.
Администратор: admin@programmer-lib.ru