Эффективный пункт 23 C ++. Предпочитать функции, не являющиеся членами, не являющимися друзьями. - PullRequest
34 голосов
/ 13 мая 2011

Размышляя над некоторыми фактами о дизайне классов, в частности о том, должны ли функции быть членами или нет, я изучил Effective c ++ и обнаружил пункт 23, а именно: «Предпочитать не являющиеся членами функции, не являющиеся друзьями, функциям-членам. Прочитав это на собственном примере с веб-браузером, имело некоторый смысл, однако вспомогательные функции (названные в книге как функции, не являющиеся членами) в этом примере изменяют состояние класса, не так ли?

  • Итак, первый вопрос, не должны ли они быть членами тогда?

  • Читая немного дальше, он рассматривает функции STL и, действительно, некоторые функции, которые не реализованы некоторыми классами, реализованы в stl. Следуя идеям книги, они превращаются в некоторые удобные функции, которые упакованы в некоторые разумные пространства имен, такие как std::sort, std::copy из algorithm. Например, класс vector не имеет функции sort, и каждый использует функцию stl sort, поэтому он не является членом векторного класса. Но можно также распространить те же рассуждения на некоторые другие функции в векторном классе, такие как assign, чтобы их можно было реализовать не как член, а как вспомогательную функцию. Однако это также меняет внутреннее состояние объекта, например сортировку, с которой он работает. Так в чем же причина этого тонкого, но важного (я полагаю) вопроса.

Если у вас есть доступ к книге, не могли бы вы прояснить эти моменты для меня?

Ответы [ 7 ]

36 голосов
/ 13 мая 2011

Доступ к книге ни в коем случае не нужен.

Проблемы, с которыми мы здесь имеем дело, Зависимость и Повторное использование .

Вправильно разработанное программное обеспечение, вы пытаетесь изолировать элементы друг от друга, чтобы уменьшить зависимости, потому что зависимости необходимо преодолеть, когда необходимо внести изменения.

В правильно разработанном программном обеспечении вы применяете Принцип СУХОЙ (не повторяйте себя), потому что, когда изменение необходимо, больно и подвержено ошибкам повторять его в дюжине разных мест.

«Классическое» ОО-мышление становится все более и болееплохо справляется с зависимостями.Имея множество методов, напрямую зависящих от внутренних элементов класса, малейшее изменение подразумевает полное переписывание.Так не должно быть.

В C ++ STL (не вся стандартная библиотека) была разработана с явными целями:

  • вырезание зависимостей
  • разрешить повторное использование

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

Например, если вы думаете о требованиях алгоритма sort.Для реализации, используемой (в общем) STL, требуется (из контейнера):

  • эффективный доступ к элементу по заданному индексу: Произвольный доступ
  • возможностьпоменяйте местами два элемента: не ассоциативный

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

Какие контейнеры в C ++ удовлетворяют этому?

  • базовый массив C
  • deque
  • vector

И любой контейнер, который вы можете написать, если вы обратите внимание на эти детали.

Было бы расточительно, не так ли, переписать (копировать / вставить / настроить) sort для каждого из них?

Обратите внимание, например, что есть метод std::list::sort.Зачем ?Поскольку std::list не предлагает произвольный доступ (неофициально myList[4] не работает), поэтому алгоритм sort from не подходит.

19 голосов
/ 13 мая 2011

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

Значительное повышение эффективности за счет реализации чего-либо в качестве функции-члена (или друга) означает, что для него очень полезно знать внутреннее состояние класса.

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

11 голосов
/ 13 мая 2011

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

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

Кстати, вы должны купить свою копию Effective C ++, это отличная книга, но не пытайтесь всегда соблюдать все пункты этой книги.Объектно-ориентированное проектирование - как хорошие практики (из книг и т. Д.), Так и опыт (я думаю, что это также где-то написано на Effective C ++).

3 голосов
/ 13 мая 2011

Итак, первый вопрос, не должны ли они быть членами, чем?

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

распространить те же рассуждения на некоторые другие функции в векторном классе

Это правда, есть некоторые функции-члены стандартных контейнеров, которые могли бы быть свободными функциями.Например, vector::push_back определяется в терминах insert и, безусловно, может быть реализовано без частного доступа к классу.В этом случае, однако, push_back является частью абстрактной концепции, BackInsertionSequence, которую реализует этот вектор.Такие общие концепции пересекают дизайн отдельных классов, поэтому, если вы разрабатываете или реализуете свои собственные общие концепции, которые могут повлиять на то, где вы размещаете функции.

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

2 голосов
/ 26 мая 2011

Различные мысли:

  • Приятно, когда не члены работают через открытый API класса, так как это уменьшает объем кода, который:
    • необходимо тщательно контролировать, чтобы гарантироватьинварианты класса,
    • должны быть изменены, если реализация объекта изменена.
  • Когда этого недостаточно, не член может все еще быть friend.
  • Написание функции, не являющейся членом, как правило, менее удобно, поскольку члены не имеют неявного охвата, НО, если учесть эволюцию программы:
    • Когда-то функция не является членомсуществует и осознается, что такая же функциональность будет полезна для других типов, как правило, очень легко преобразовать функцию в шаблон и сделать ее доступной не только для обоих типов, но и для произвольных будущих типов.Другими словами, шаблоны, не являющиеся членами, допускают еще более гибкое повторное использование алгоритма, чем полиморфизм во время выполнения / виртуальная диспетчеризация: шаблоны позволяют что-то, известное как типирование утки .
    • Существующий тип, содержащий полезный элементфункция поощряет вырезание и вставку для других типов, которые хотели бы аналогичного поведения, потому что большинство способов преобразования функции для повторного использования требуют, чтобы каждому неявному доступу к члену был предоставлен явный доступ к определенному объекту, которыйбудет более утомительным 30 + секунд для программиста ....
  • Функции-члены допускают нотацию object.function(x, y, z), что, IMHO, очень удобно, выразительно и интуитивно понятно.Они также лучше работают с функциями обнаружения / завершения во многих IDE.
  • Разделение как функции-члены и функции, не являющиеся членами, может помочь передать основную природу класса, его инвариантов и фундаментальных операций, а также логически сгруппироватьдополнительные и, возможно, специальные «удобные» функции.Примите во внимание мудрость Тони Хоара:

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

    • Здесь использование не-члена не обязательно намного сложнее, но вам нужно больше думать о том, как вы получаете доступ к членуданные и частные / защищенные методы и почему, и какие операции являются основополагающими.Подобный поиск души улучшил бы дизайн и с помощью функций-членов, просто легче быть ленивым по поводу: - /.
  • Поскольку функциональность, не являющаяся членом, расширяется в изощренности или приобретает дополнительныезависимости, функции могут быть перемещены в отдельные заголовки и файлы реализации, даже библиотеки, так что пользователи основных функций «платят» только за те части, которые им нужны.

(ответ Omnifarious:обязательно прочитайте, трижды, если это плохо для вас.)

2 голосов
/ 13 мая 2011

Мотивация проста: поддерживать последовательный синтаксис. Как класс развивается или используется, различные вспомогательные функции, не являющиеся членами появляются; Вы не хотите изменять интерфейс класса, чтобы добавить что-то например, toUpper для строкового класса. (В случае std::string, конечно, вы не можете.) Скотт беспокоится, что когда это случается, вы в конечном итоге с противоречивым синтаксисом:

s.insert( "abc" );
toUpper( s );

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

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

1 голос
/ 13 мая 2011

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

...