Не дружественные, не являющиеся членами функции увеличивают инкапсуляцию?
В статье " Как функции, не являющиеся членами, улучшают инкапсуляцию", Скотт Мейерс утверждает, что не существует способа предотвратить "возникновение" функций, не являющихся членами.
Проблемы с синтаксисом
Если вы похожи на многих людей, с которыми я обсуждал эту проблему, у вас, вероятно, возникнут сомнения относительно синтаксических последствий моего совета о том, что функции, не являющиеся членами, не являющимися членами, следует отдавать предпочтение функциям-членам, даже если вы покупаете мой спор об инкапсуляции. Например, предположим, что класс Wombat поддерживает функциональность как еды, так и сна. Далее предположим, что функциональность приема пищи должна быть реализована как функция-член, но функциональность ожидания может быть реализована как функция-член или не-членская функция. Если вы последуете моему совету сверху, вы бы заявили о таких вещах:
class Wombat { public: void eat(double tonsToEat); void sleep(double hoursToSnooze); }; w.eat(.564); w.sleep(2.57);
Ах, единообразие всего этого! Но это единообразие вводит в заблуждение, потому что в мире есть больше функций, чем мечтает ваша философия.
Проще говоря, функции, не являющиеся членами, бывают. Давайте продолжим с примером Wombat. Предположим, вы пишете программное обеспечение для моделирования этих извлекающих существ и представляете, что одна из вещей, которую вам часто нужно делать вомбатам, - это спать ровно полчаса. Очевидно, что вы можете засорять свой код звонками на
w.sleep(.5)
, но это будет много.5s для ввода, и, во всяком случае, что, если это магическое значение должно измениться? Существует несколько способов решения этой проблемы, но, возможно, самый простой - определить функцию, которая инкапсулирует детали того, что вы хотите сделать. Предполагая, что вы не являетесь автором Wombat, функция обязательно должна быть не-членом, и вам придется вызывать ее так:void nap(Wombat& w) { w.sleep(.5); } Wombat w; nap(w);
И вот оно, твое ужасное синтаксическое несоответствие. Когда вы хотите кормить своих вомбатов, вы делаете вызовы функций-членов, но когда вы хотите, чтобы они дремали, вы делаете вызовы, не являющиеся членами.
Если вы немного поразмышляете и будете честны с самим собой, вы признаете, что у вас есть предполагаемое несоответствие со всеми нетривиальными классами, которые вы используете, потому что ни один класс не имеет каждой функции, желаемой каждым клиентом. Каждый клиент добавляет по крайней мере несколько своих собственных удобных функций, и эти функции всегда не являются членами. Программисты C++ привыкли к этому, и они ничего не думают об этом. Некоторые вызовы используют синтаксис члена, а некоторые используют синтаксис, не являющийся членом. Люди просто смотрят, какой синтаксис подходит для функций, которые они хотят вызвать, а затем они их вызывают. Жизнь идет. Это особенно актуально в части STL стандартной библиотеки C++, где некоторые алгоритмы являются функциями-членами (например, size), некоторые являются функциями, не являющимися членами (например, уникальными), а некоторые - обе (например, find). Никто не моргает. Даже ты.
Я не могу действительно обернуть голову вокруг того, что он говорит в жирном / курсивном предложении. Почему это обязательно должно быть реализовано как не член? Почему бы просто не унаследовать свой собственный класс MyWombat от класса Wombat и сделать nap()
функционировать членом MyWombat?
Я только начинаю с C++, но именно так я бы, вероятно, сделал это на Java. Разве это не путь в C++? Если нет, то почему?
3 ответа
Теоретически, вы могли бы сделать это, но на самом деле не хотите. Давайте рассмотрим, почему вы не хотите этого делать (на данный момент в исходном контексте -C++98/03 и игнорируем дополнения в C++11 и новее).
Прежде всего, это будет означать, что по существу все классы должны быть написаны, чтобы действовать как базовые классы - но для некоторых классов это просто паршивая идея, и она может даже идти вразрез с основным намерением (например, что-то, предназначенное для реализации образец легковеса).
Во-вторых, это сделало бы большинство наследства бессмысленными. Для наглядного примера многие классы в C++ поддерживают ввод / вывод. В нынешнем виде идиоматический способ сделать это - перегрузить operator<<
а также operator>>
как свободные функции. Прямо сейчас, цель iostream состоит в том, чтобы представить что-то, что по крайней мере смутно похоже на файл - что-то, в что мы можем записывать данные и / или из которых мы можем читать данные. Если бы мы поддерживали ввод / вывод через наследование, это также означало бы все, что можно прочитать / записать во что-то неопределенно похожее на файл.
Это просто не имеет никакого смысла. Iostream представляет что-то, по меньшей мере, неопределенно похожее на файл, а не все виды объектов, из которых вы можете прочитать или записать файл.
Хуже того, это сделало бы почти всю проверку типов компилятором практически бессмысленной. Просто, например, написание distance
возражать в person
Объект не имеет смысла - но если они оба поддерживают ввод / вывод, будучи производными от iostream, то у компилятора не будет способа отделить это от того, что действительно имело смысл.
К сожалению, это только верхушка айсберга. Когда вы наследуете от базового класса, вы наследуете ограничения этого базового класса. Например, если вы используете базовый класс, который не поддерживает назначение копирования или конструкцию копирования, объекты производного класса также не будут / не могут.
Продолжая предыдущий пример, это будет означать, что если вы хотите выполнить ввод-вывод для объекта, вы не можете поддерживать конструкцию копирования или назначение копирования для этого типа объекта.
Это, в свою очередь, означает, что объекты, поддерживающие ввод-вывод, будут не связаны с объектами, поддерживающими помещение в коллекции (т. Е. Для коллекций требуются возможности, запрещенные iostreams).
Итог: мы почти сразу же сталкиваемся с совершенно неуправляемым беспорядком, где ни одно из нашего наследования больше не имело бы никакого реального смысла, а проверка типов компилятором стала бы практически полностью бесполезной.
Потому что тогда вы создаете очень сильную зависимость между вашим новым классом и оригинальным Wombat. Наследование не обязательно хорошо; это вторая сильная связь между любыми двумя объектами в C++. Только friend
декларации сильнее.
Я думаю, что большинство из нас сделали двойную попытку, когда Мейерс впервые опубликовал эту статью, но сейчас общепризнано, что это правда. В мире современного C++ ваш первый инстинкт не должен быть производным от класса. Вывод является последним средством, если только вы не добавляете новый класс, который действительно является специализацией существующего класса.
Вопросы разные в Java. Там вы наследуете. У тебя действительно нет другого выбора.
Ваша идея не работает по всем направлениям, как описывает Джерри Коффин, однако она жизнеспособна для простых классов, которые не являются частью иерархии, таких как Wombat
Вот.
Есть несколько опасностей, на которые стоит обратить внимание:
Нарезка - если есть функция, которая принимает
Wombat
по значению, то вы должны отрезатьmyWombat
лишние придатки и они не отрастают. Это не происходит в Java, в которой все объекты передаются по ссылке.Указатель базового класса - если
Wombat
является неполиморфным (то есть без V-таблицы), это означает, что вы не можете легко смешатьWombat
а такжеmyWombat
в контейнере. Удаление указателя не удалится должным образомmyWombat
сорта. (Однако вы могли бы использоватьshared_ptr
который отслеживает пользовательское удаление).Несоответствие типов: если вы пишете какие-либо функции, которые принимают
myWombat
тогда они не могут быть вызваны сWombat
, С другой стороны, если вы напишите свою функцию, чтобы принятьWombat
тогда вы не можете использовать синтаксический сахарmyWombat
, Кастинг не исправляет это; Ваш код не будет правильно взаимодействовать с другими частями интерфейса.
Способ избежать всех этих опасностей состоит в том, чтобы использовать сдерживание вместо наследования: myWombat
будет иметь Wombat
закрытый член, и вы пишете функции пересылки для любых свойств Wombat, которые вы хотите предоставить. Это больше работы с точки зрения проектирования и обслуживания myWombat
учебный класс; но он исключает возможность для любого использования вашего класса по ошибке и позволяет обойти такие проблемы, как содержащийся класс, который нельзя скопировать.
Для полиморфных объектов в иерархии у вас нет проблем с нарезкой и указателем базового класса, хотя проблема несоответствия типов все еще существует. На самом деле это хуже. Предположим, что иерархия:
Animal <-- Marsupial <-- Wombat <-- NorthernHairyNosedWombat
Вы приходите и выводите myWombat
от Wombat
, Однако это означает, что NorthernHairyNosedWombat
родной брат myWombat
в то время как это было дитя Wombat
,
Так что любые приятные функции сахара, которые вы добавляете в myWombat
не могут быть использованы NorthernHairyNosedWombat
тем не мение.
Резюме: ИМХО выгоды не стоят того беспорядка, который он оставляет позади.