Эффективный пункт 23 C++. Предпочитать функции, не являющиеся членами, не являющимися друзьями.
Размышляя над некоторыми фактами о дизайне классов, в частности о том, должны ли функции быть членами или нет, я изучил Effective C++ и нашел пункт 23, а именно: "Предпочитать не являющиеся членами функции, не являющиеся друзьями, функциям-членам. Прочитав это на собственном примере с веб-браузером, имело некоторый смысл, однако вспомогательные функции (так называемые функции, не являющиеся членами в этой книге) в этом примере изменяют состояние класса, не так ли?
Итак, первый вопрос, не должны ли они быть членами, чем?
Читая немного дальше, он рассматривает функции STL и, действительно, некоторые функции, которые не реализованы некоторыми классами, реализованы в stl. Следуя идеям книги, они превращаются в некоторые удобные функции, которые упакованы в некоторые разумные пространства имен, такие как
std::sort
,std::copy
отalgorithm
, Напримерvector
класс не имеетsort
функция и один использует STLsort
функция, так что не является членом векторного класса. Но можно также распространить те же рассуждения на некоторые другие функции в векторном классе, такие какassign
так что это также может быть реализовано не как элемент, а как вспомогательная функция. Однако это также изменяет внутреннее состояние объекта, например сортировку, с которой он работает. Так в чем же причина этого тонкого, но важного (я полагаю) вопроса.
Если у вас есть доступ к книге, не могли бы вы прояснить эти моменты для меня?
7 ответов
Доступ к книге ни в коем случае не является необходимым.
Проблемы, с которыми мы здесь имеем дело, это зависимость и повторное использование.
В хорошо спроектированном программном обеспечении вы пытаетесь изолировать элементы друг от друга, чтобы уменьшить зависимости, потому что зависимости являются препятствием, которое необходимо преодолеть, когда необходимы изменения.
В хорошо спроектированном программном обеспечении вы применяете принцип СУХОГО (не повторяйте себя), потому что, когда изменение необходимо, больно и подвержено ошибкам повторять его в дюжине разных мест.
"Классический" ОО-образ мышления все в большей степени плохо справляется с зависимостями. Имея множество методов, напрямую зависящих от внутренних элементов класса, малейшее изменение подразумевает полное переписывание. Так не должно быть.
В C++ STL (не вся стандартная библиотека) была разработана с явными целями:
- сокращение зависимости
- разрешить повторное использование
Таким образом, контейнеры предоставляют четко определенные интерфейсы, которые скрывают свои внутренние представления, но при этом обеспечивают достаточный доступ к информации, которую они инкапсулируют, чтобы на них могли выполняться алгоритмы. Все модификации производятся через интерфейс контейнера, так что инварианты гарантированы.
Например, если вы думаете о требованиях sort
алгоритм. Для реализации, используемой (в общем) STL, требуется (из контейнера):
- эффективный доступ к элементу по заданному индексу: произвольный доступ
- возможность поменять два предмета: не ассоциативный
Таким образом, любой контейнер, который обеспечивает произвольный доступ и не является ассоциативным, (в теории) подходит для эффективной сортировки, скажем, алгоритмом быстрой сортировки.
Какие контейнеры в C++ удовлетворяют этому?
- основной C-массив
deque
vector
И любой контейнер, который вы можете написать, если вы обратите внимание на эти детали.
Было бы расточительно, не так ли, переписать (копировать / вставить / настроить) sort
для каждого из тех?
Обратите внимание, например, что есть std::list::sort
метод. Зачем? Так как std::list
не предлагает произвольный доступ (неофициально myList[4]
не работает), таким образом, sort
Из алгоритма не подходит.
Я использую критерии: если функция может быть реализована значительно более эффективно, будучи функцией-членом, тогда она должна быть функцией-членом. ::std::sort
не соответствует этому определению. На самом деле, нет никакой разницы в эффективности при ее реализации как внешней, так и внутренней.
Значительное повышение эффективности за счет реализации чего-либо в качестве функции-члена (или друга) означает, что для него очень полезно знать внутреннее состояние класса.
Частью искусства проектирования интерфейса является искусство поиска самого минимального набора функций-членов, так что все операции, которые вы, возможно, захотите выполнить над объектом, могут быть реализованы достаточно эффективно с их точки зрения. И этот набор не должен поддерживать операции, которые не должны выполняться над классом. Таким образом, вы не можете просто реализовать набор функций получения и установки и назвать это хорошо.
Я думаю, что причина этого правила заключается в том, что при использовании функций-членов вы можете слишком сильно полагаться на внутренние компоненты класса. Изменение состояния класса не является проблемой. Реальная проблема заключается в объеме кода, который вам нужно изменить, если вы измените какое-то частное свойство внутри вашего класса. Сохранение интерфейса класса (публичные методы) как можно меньше уменьшает как объем работы, который вам потребуется в таком случае, так и риск сделать что-то странное с вашими личными данными, оставив вам экземпляр в несовместимом состоянии,
AtoMerZ также прав, функции, не являющиеся членами-не-друзьями, могут быть шаблонизированы и повторно использованы и для других типов.
Кстати, вы должны купить свою копию Effective C++, это отличная книга, но не пытайтесь всегда соблюдать все пункты этой книги. Объектно-ориентированное проектирование - как хорошие практики (из книг и т. Д.), Так и опыт (я думаю, что это также где-то написано в Effective C++).
Разные мысли:
- Хорошо, когда не члены работают через открытый API класса, так как это уменьшает объем кода, который:
- необходимо тщательно контролировать, чтобы обеспечить классовые инварианты,
- должен быть изменен, если реализация объекта изменена.
- Когда это не достаточно хорошо, не-член все еще может быть сделан
friend
, - Написание функции, не являющейся членом, обычно не очень удобно, так как члены не имеют неявного охвата, НО, если учесть эволюцию программы:
- Когда существует функция, не являющаяся членом, и осознается, что та же функциональность будет полезна для других типов, обычно очень легко преобразовать функцию в шаблон и сделать ее доступной не только для обоих типов, но и для произвольных будущих типов. Иными словами, шаблоны, не являющиеся членами, допускают еще более гибкое повторное использование алгоритма, чем полиморфизм во время выполнения / виртуальная диспетчеризация: шаблоны позволяют использовать функцию, известную как типизирование утки.
- Существующий тип, содержащий полезную функцию-член, поощряет вырезание и вставку для других типов, которые хотели бы аналогичного поведения, потому что большинство способов преобразования функции для повторного использования требуют, чтобы для каждого неявного доступа к члену был сделан явный доступ к определенному объекту, что будет более 30 секунд для программиста....
- Функции-члены позволяют
object.function(x, y, z)
обозначения, которые ИМХО очень удобны, выразительны и интуитивно понятны. Они также лучше работают с функциями обнаружения / завершения во многих IDE. Разделение на функции-члены и функции, не являющиеся членами, может помочь передать основную природу класса, его инварианты и фундаментальные операции, а также логически сгруппировать дополнительные и, возможно, специальные "удобные" функции. Рассмотрим мудрость Тони Хоара:
"Существует два способа конструирования программного обеспечения: один из них заключается в том, чтобы сделать его настолько простым, чтобы в нем явно не было недостатков, а другой способ - сделать его настолько сложным, чтобы не было явных недостатков. Первый способ гораздо сложнее ".- Здесь использование не-участника не обязательно намного сложнее, но вам нужно больше думать о том, как вы получаете доступ к данным-членам и о частных / защищенных методах и почему, и какие операции являются основополагающими. Такой поиск души улучшил бы дизайн и с помощью функций-членов, просто проще быть ленивым:-/.
По мере того, как функциональность, не являющаяся членом, расширяется по сложности или приобретает дополнительные зависимости, функции можно перемещать в отдельные заголовки и файлы реализации, даже в библиотеки, поэтому пользователи основных функций "платят" только за использование тех частей, которые им нужны.
(Ответ Omnifarious должен быть прочитан трижды, если он для вас новый.)
Итак, первый вопрос, не должны ли они быть членами, чем?
Нет, это не следует В идиоматическом дизайне класса C++ (по крайней мере, в идиомах, используемых в Effective C++), функции, не являющиеся членами, не являются дружественными, расширяют интерфейс класса. Их можно считать частью общедоступного API для класса, несмотря на то, что они не нуждаются и не имеют частного доступа к классу. Если этот дизайн "не ООП" по некоторому определению ООП, тогда ОК, идиоматический C++ не ООП по этому определению.
растянуть те же рассуждения на некоторые другие функции в векторном классе
Это правда, есть некоторые функции-члены стандартных контейнеров, которые могли бы быть свободными функциями. Например vector::push_back
определяется с точки зрения insert
и, конечно, может быть реализовано без частного доступа к классу. В этом случае, однако, push_back
является частью абстрактной концепции, BackInsertionSequence
, этот вектор реализует. Такие общие концепции пронизывают дизайн отдельных классов, поэтому, если вы разрабатываете или реализуете свои собственные общие концепции, которые могут повлиять на то, где вы размещаете функции.
Конечно, есть части стандарта, которые, возможно, должны были быть другими, например, std::string имеет слишком много функций-членов. Но то, что сделано, сделано, и эти классы были разработаны прежде, чем люди действительно утвердились в том, что мы сейчас можем назвать современным стилем C++. Класс работает в любом случае, так что практической пользы вы можете получить, беспокоясь о разнице.
Мотивация проста: поддерживать последовательный синтаксис. По мере развития или использования класса будут появляться различные вспомогательные функции, не являющиеся членами; Вы не хотите изменять интерфейс класса, чтобы добавить что-то вроде toUpper
к строковому классу, например. (В случаеstd::string
Конечно, вы не можете. Скотт беспокоится, что когда это произойдет, вы получите непоследовательный синтаксис:
s.insert( "abc" );
toUpper( s );
Используя только бесплатные функции, объявляя их друзьями по мере необходимости, все функции имеют одинаковый синтаксис. Альтернативой может быть изменение определения класса каждый раз, когда вы добавляете вспомогательную функцию.
Я не совсем убежден. Если класс хорошо спроектирован, у него есть базовая функциональность, пользователю ясно, какие функции являются частью этой базовой функциональности, а какие являются дополнительными вспомогательными функциями (если таковые существуют). Глобально, строка является своего рода особым случаем, потому что она предназначена для решения многих различных проблем; Я не могу представить, что это имеет место для многих классов.
Я думаю, что сортировка не реализована как функция-член, потому что она широко используется не только для векторов. Если бы они использовали его в качестве функции-члена, им пришлось бы каждый раз заново его реализовывать для каждого контейнера, использующего его. Так что я думаю, что для облегчения реализации.