Анимация
JavaScript


Главная  Библионтека 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 [ 78 ] 79 80 81 82 83 84 85

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

• at (2)

• clear

• empty

• length

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

"Минутку! - слышу я некоторых поклонников стандарта. - Не так быстро! Вы что. не знаете, что basic string разработан таким образом, чтобы соответствовать требованиям к контейнерам, предъявляемым стандартом С++, а эти требования гласят, что некоторые функции должны быть членами? Так что не вводите читателей в заблуждение! Эти функции являются членами, хотите вы того или нет, и с этим ничего нельзя поделать!" Да, конечно, это так, но давайте отвлечемся от эго го замечания для того, чтобы беспрепятственно продолжить изучение поднятого в задаче вопроса, и будем считать, что этого требования просто нет.

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

Рассмофим функцию empty. Можем ли мы реализовать ее как обычную функцию, не являющуюся другом класса? Конечно. Стандарт требует, чтобы функция basi c string: : empty вела себя следующим образом [С++03, §21.3.3/14]:

Возвращаемое значение: size О == О

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

template<class charT, class traits, class Allocator> bool empty(const basic string<charT, traits, Allocator>& s) { return S.sizeO == 0;

Конечно, если у нас нет функции size, то реализация empty как обычной функции,

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

tempiate<class charT, class traits, class Allocator> bool empty(const basic string<charT, traits, Allocator>& s) { return S.beginO == s.end ();

Достижима и большая степень обобщенности.

tempiate<typename т>

bool empty( const т& t ) {

return t.beginO == t.endO ;

Эта окончательная версия никак не связана со строками; все, что она требует от своего аргумента, - это наличие функций begi п и end. Открытые функции-члены класса должны обеспечивать необходимую и достаточную функциональность. Как мы увидим в дальнейшем, этот вопрос еще не раз возникнет при рассмотрении других



функций. (Далее в этой задаче я больше не буду записывать шаблоны функций как полностью обобшенные; все они будут связаны с классом basic string. Однако они вполне могут быть обобщены, и мы увидим, что для этого есть масса причин.)

Обратите внимание, что хотя мы можем сделать size членом и реализовать функ-цию-не член empty с ее помощью, мы не можем сделать обратное. В ряде рассматриваемых здесь случаев имеется группа взаимосвязанных функций, некоторые из которых являются членами, а остальные - обычными функциями, которые реализованы с использованием упомянутых функций-членов. Какие именно функции должны быть членами? Мой совет - выбрать наиболее гибкие функции, не приводящие к потере эффективности, которые обеспечат нам гибкий фундамент, на котором можно будет легко построить все остальные функции. В нашем случае мы выбрали si ze в качестве функции-члена, поскольку ее результат всегда может быть кэширован (стандарт поощряет применение кэширования при реализации, поскольку функция size "должна" выполняться за постоянное время), и в этом случае реализация empty посредством функции size не менее эффективна, чем любой способ ее реализации с использованием полного непосредственного доступа ко внутренним данным класса.

А что можно сказать о следующей функции из условия задачи - функции at? К ней применимы те же рассуждения. Как для const, так и для не-const версии стандарт требует следующего:

Генерация исключений: out of. range, если pos>= sizeQ.

Возвращаемое значение: operator[](pos).

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

tempiate<class charT, class traits, class Allocator> typename basic stri ng<charT,traits,Al1ocator>::const reference at(const basic stri ng<charT,traits,All ocator>&s, typename basic string<charT,traits,

Allocator>::size type pos)

if(pos >= S.sizeO) throw out of range("dont do that"); return s[pos];

template<class charT, class traits, class Allocator> typename basic string<charT, traits, Al1ocator>::reference at(basic string<charT, traits, Allocator>& s, typename basic„string<charT, traits,

Allocator>::size type pos)

if (pos >= S.sizeO)

throw out of range("l said, dont do that"); return s[pos];

Что касается clear, то это всего лишь е rase (begi п() , end()) - не больше, не меньше. Реализация в виде обычной функции, не являющейся другом класса, - не более чем простенькое упражнение для читателя.

Осталось рассмофеть функцию 1 ength. Здесь тоже все просто - так как эта функция просто возвращает тот же результат, что и si ze. Кроме того, обратите внимание, что другие контейнеры не имеют функции-члена length, и в интерфейсе basi с.....string она играет роль "чисто строковой функции". Если мы сделаем ее не членом, то такая функция будет применима к любому контейнеру. Эта функция не

Обратите внимание, что это заключение не применимо для класса list, поскольку время работы list: : size линейно зависит от количества элементов в списке.



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

Резюмируя, давайте рассмотрим преимущества и недостатки, которые мы получаем, переделывая функции-члены наподобие empty, в обычные функции. Начнем с того, что даже если мы можем переписать функцию-член как обычную функцию, не являющуюся другом класса, без потери эффективности, то почему мы должны этим заниматься? Какие реальные или потенциальные преимущества сможем мы при этом получить?

/. Простота. Сделав функции не членами, мы сокращаем код, который должны писать и сопровождать. Мы можем написать функцию empty раз и навсегда. Зачем нам много раз писать разные варианты функции -basi cstri ng:: empty, vector: : empty, list:: empty и так далее, включая написание этой функции для всех новых STL-совместимых контейнеров, которые могут появиться в каких-то библиотеках сторонних производителей или даже в будущих версиях стандарта С++?

Заметим, что у этого преимущества имеются некоторые офаничения. Некоторые из функций, такие как at, не способны обеспечить одинаковое время работы, поскольку сложность функции будет варьироваться в зависимости от используемого контейнера. Так, для тар функция имеет логарифмическое время работы, в го время как для vector время работы функции является константой. Далее, может оказаться так, что не все контейнеры, имеющиеся в настоящее время или те, которые будут разработаны в будущем, будут предоставлять необходимый для работы функции интерфейс. Как видно из приведенного выше примера, функция empty, не являющаяся членом класса, использует функцию-член size и не будет работать с контейнерами, которые эту функцию не поддерживают; кроме того, для контейнеров, которые имеют достаточный, но отличный от требуемого интерфейс, потребуются специализированные или перефуженные версии функций.

2 Последовательность. Таким образом мы избегаем неоправданных несовместимостей между алгоритмами, используемыми в различных контейнерах, а также между алгоритмами, используемыми в функциях-членах и обьиных функциях (некоторые реально существующие несовместимости такого рода между функциями-членами и функциями-не членами указаны в [MeyersO!]). Если требуется настройка поведения функций, то ее можно выполнить с помощью специализации или перефузки шаблона функции.

3. Инкапсуляция. Использование обычных функций повышает степень инкапсуляции (что доказано в [MeyersOO]).

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

4. Засорение пространств имен. Поскольку empty - достаточно распространенное имя, размещение его в области видимости пространства имен приводит к риску засорения последнего - разве будут все функции с именем empty иметь одну и ту же семантику? Однако, во-первых, последовательность семантики - Хорошая Вещь, а во-вторых, разрешение перефузки - хорошее противоядие против неоднозначностей, так что засорение пространства имен не такая большая проблема, как многие считали ранее. Действительно, собирая все имена функций в одно место и предоставляя общую их реализацию, мы на самом деле не столько засоряем пространство имен, сколько, как отмечает Мейерс, очищаем сами функции, собирая их в одно.м месте и тем самым помогая избегать очевидных и необоснованных случаев их несовместимости.

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



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 [ 78 ] 79 80 81 82 83 84 85