Широкомасштабное использование совета Мейера, чтобы отдать предпочтение функциям, не являющимся членами, не являющимися друзьями? - PullRequest
41 голосов
/ 29 сентября 2010

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

Я делал это с хорошим эффектом в нескольких небольших проектах, но мне интересно, насколько хорошо это работает в больших масштабах.Существуют ли какие-либо крупные, хорошо известные проекты C ++ с открытым исходным кодом, на которые я могу взглянуть и, возможно, сослаться на них, где строго следуют этому совету?

Обновление: спасибо за все комментарии, но на самом деле меня интересует не столько мнение, сколько выяснение того, насколько хорошо это работает на практике в более широком масштабе.Ответ Ника в этом отношении наиболее близок, но я бы хотел увидеть код.Любое подробное описание практического опыта (позитивные, негативные, практические соображения и т. Д.) Также будет приемлемым.

Ответы [ 8 ]

11 голосов
/ 29 сентября 2010

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

В качестве примера рассмотрим пример классического полиморфизма: базовый класс Shape с подклассами и виртуальную функцию Draw (). В реальном мире Draw () должен был бы взять некоторый контекст рисования и потенциально знать о состоянии других нарисованных вещей или о приложении в целом. Как только вы поместите все это в каждую реализацию подкласса Draw (), вы, вероятно, будете иметь некоторое перекрытие кода, или большая часть вашей реальной логики Draw () будет находиться в базовом классе или где-то еще. Затем учтите, что если вы хотите повторно использовать часть этого кода, вам нужно будет предоставить больше точек входа в интерфейс и, возможно, загрязнить функции другим кодом, не связанным с рисованием фигур (например, логикой корреляции рисования нескольких фигур). ). Вскоре это будет беспорядок, и вы захотите, чтобы у вас была функция рисования, которая взяла бы Shape (и контекст, и другие данные) вместо этого, и у Shape были только функции / данные, которые были полностью инкапсулированы и не использовали или ссылались внешние объекты.

В любом случае, это мой опыт / совет, для чего это стоит.

9 голосов
/ 29 сентября 2010

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

Если вы можете отделить алгоритмы от структур данных (или, если выразить это иначе, если вы можете отделить то, что вы делаете с объектами, от того, как онивнутренним состоянием манипулируют), вы можете уменьшить связь между вашими классами и использовать преимущества общего кода.

Скотт Мейерс не единственный автор, который высказался в пользу этого принципа;У Херба Саттера тоже есть особенности, особенно в Unstrung * Monoliths , что заканчивается указанием:

Где это возможно, предпочитайте писать функции, не являющиеся членами.

Я думаю, один из лучших примеров ненужной функции-члена из этой статьи - std::basic_string::find;на самом деле нет никаких причин для его существования, поскольку std::find обеспечивает точно такую ​​же функциональность.

5 голосов
/ 16 октября 2010

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

Библиотека OpenCV огромна и широко известна в своей области.

4 голосов
/ 13 октября 2010

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

Рассмотрим, например, функции-члены контейнера последовательности insert и push_back.Существует как минимум два подхода к реализации push_back:

  1. Он может просто вызывать insert (его поведение определяется в любом случае в терминах insert)
  2. Это может сделатьвся работа, которую insert сделает (возможно, вызовет частные вспомогательные функции) без фактического вызова insert

Очевидно, что при реализации контейнера последовательности вы, вероятно, захотите использовать первый подход.push_back - это просто особая форма insert, и (насколько мне известно) вы не сможете получить никакого выигрыша в производительности, внедрив push_back другим способом (по крайней мере, для list, deque, или vector).

Однако для тщательного тестирования такого контейнера необходимо отдельно протестировать push_back: поскольку push_back является функцией-членом, она может изменять любое внутреннее состояниеконтейнера.С точки зрения тестирования, вы должны (должны?) Предположить, что push_back реализован с использованием второго подхода, поскольку возможно, что он может быть реализован с использованием второго подхода.Нет гарантии, что он реализован в терминах insert.

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

2 голосов
/ 18 октября 2010

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

1 голос
/ 27 марта 2015

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

То есть, если у вас есть шаблон класса type<T>:

template<class T>
struct type {
  void friend foo(type<T> a) {}
};

и тип, неявно преобразуемый в type<T>, например:

template<class T>
struct convertible_to_type {
  operator type<T>() { }
};

Следующее работает как ожидалось:

auto t = convertible_to_type<int>{};
foo(t);  // t is converted to type<int>

Однако, если вы сделаете foo a не-друга функцией:

template<class T>
void foo(type<T> a) {}

, то следующее неwork:

auto t = convertible_to_type<int>{};
foo(t);  // FAILS: cannot deduce type T for type

Поскольку вы не можете вывести T, функция foo удаляется из набора разрешений перегрузки, то есть: функция не найдена, что означает, что неявное преобразование не сработает.

1 голос
/ 15 октября 2010

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

У меня есть большое сочувствие к позиции, занятой Джонатаном Гринспаном, но я хочу сказать немного больше об этом, чем это можно обоснованно сделать в комментариях.

Сначала - «хорошо сказано» Альфу Штейнбаху, который присоединился к«Только слишком упрощенные карикатуры на их точки зрения могут показаться противоречивыми. Из-за того, что это того стоит, я не согласен со Скоттом Мейерсом по этому вопросу; насколько я понимаю, он здесь слишком обобщает, или он был».

Скотт, Херб и т. Д. Делали эти замечания, когда мало кто понимал компромиссы или альтернативы, и они делали это с непропорциональной силой.Были проанализированы некоторые неприятности, с которыми люди сталкивались при разработке кода, и был рационально выведен новый подход к проектированию, направленный на эти проблемы .Давайте вернемся к вопросу о том, были ли недостатки позже, но сначала - стоит сказать, что рассматриваемая проблема, как правило, была небольшой и нечастой: функции, не являющиеся членами, являются лишь одним небольшим аспектом разработки повторно используемого кода, и в системах масштаба предприятия яРаботая над простым написанием кода того же типа, который вы поместили бы в функцию-член в качестве не-члена, достаточно редко, чтобы сделать не-члены повторно используемыми.Для них довольно редко даже выражать алгоритмы, которые являются достаточно сложными, чтобы их стоило использовать повторно, но при этом они не были тесно связаны со спецификой класса, для которого они были разработаны, - настолько странно, что практически невозможно представить, чтобы какой-то другой класс происходил при поддержкете же операции и семантика.Часто вам также нужно шаблонировать аргументы или вводить базовый класс для абстрагирования набора необходимых операций.Оба имеют существенное влияние с точки зрения производительности, будучи встроенными по сравнению с внешними, перекомпиляцией клиентского кода.

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

Но, как лакмусовая бумажка, - сколько из этих функций, не являющихся членами, находится в том же заголовке, что и единственный классдля чего они в настоящее время применимы?Многие ли хотят абстрагировать свои аргументы с помощью шаблонов (что означает встраивание, зависимости компиляции) или базовых классов (накладные расходы на виртуальные функции), чтобы разрешить их повторное использование?Оба препятствуют тому, чтобы люди считали их повторно используемыми, но когда это не так, операции, доступные в классе, делокализованы , что может расстроить восприятие системы разработчиками: разработка часто должна разрабатывать для себя довольноразочаровывающий факт, что - «о - это будет работать только для класса X».

Итог: большинство функций-членов потенциально не могут быть повторно использованы.Большая часть корпоративного кода не разбита на чистый алгоритм по сравнению с данными с возможностью повторного использования первого.Такое разделение просто не является обязательным, полезным или, по-видимому, полезным 20 лет спустя.Это почти то же самое, что и методы get / set - они необходимы на определенных границах API, но могут представлять собой ненужную многословность, когда владение и использование кода локализовано.

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

0 голосов
/ 29 сентября 2010

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

Я категорически не согласен с этой статьей для C ++, потому что несоответствие будет раздражать, а в современной среде IDE нарушение целостности будет нарушено.

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

...