Подтвердить что ты не робот

Широкомасштабное использование совета Мейера для предпочтения функций, не являющихся членами, а не друга?

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

По возможности, предпочитайте писать функции как неподдерживаемые.

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

Ответ 6

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

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

Сначала - "хорошо сказано" Альфу Штайнбаху, который набросился на "Это только чрезмерно упрощенные карикатуры на их точки зрения, которые могут показаться в конфликте. За что это стоит, я не согласен со Скоттом Мейерсом в этом как я вижу, он чрезмерно обобщал здесь, или он был".

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

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

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

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

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

Ответ 7

Предпочитают функции, не являющиеся членами non-friend для инкапсуляции 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 функцию, отличную от друга:

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

то следующее не работает:

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

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

Ответ 8

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

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

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