Когда функции должны быть функциями-членами? - PullRequest
25 голосов
/ 28 октября 2009

У меня есть коллега в моей компании, к мнению которого я очень уважаю, но я просто не могу понять один из его предпочтительных стилей написания кода на C ++.

Например, если есть класс A, он напишет глобальные функции типа:

void foo( A *ptrToA ){}

или

void bar( const A &refToA ){}

Мой первый инстинкт при взгляде на глобальные функции, подобные этому: «Почему это не члены A?» Он будет настаивать на том, что это согласуется с рекомендациями для хорошей практики в C ++, потому что foo и bar могут выполнять все, что им нужно, используя открытый интерфейс A. Например, он будет утверждать, что это полностью соответствует с Скоттом Мейерсом Эффективные рекомендации C ++. Мне трудно согласовать это с пунктом 19 в этой книге, в котором в основном говорится, что все должно быть функцией-членом с несколькими исключениями (operator << и operator >> и функции, которые требуют динамического преобразования типов). Кроме того, хотя я согласен с тем, что функции могут делать то, что им нужно, с общедоступным интерфейсом A, по моему мнению, это во многом является результатом того, что люди пишут классы, которые имеют методы получения и установки для каждого члена данных класса А. открытый интерфейс, A - это чрезмерно прославленная структура, и вы, безусловно, можете делать с открытым интерфейсом все, что угодно. Лично я не думаю, что это следует использовать, я думаю, что это не должно поощряться.

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

Изменить: Позвольте мне привести лучший пример кода.

class Car
{
    Wheel frontLeft;
    Wheel frontRight;
    Wheel rearLeft;
    Wheel rearRight;
    Wheel spareInTrunk;

public:
    void wheelsOnCar( list< Wheel > &wheels )
    {
        wheels.push_back( frontLeft );
        wheels.push_back( frontRight);
        wheels.push_back( rearLeft);
        wheels.push_back( rearRight);
    }
    const Wheel & getSpare(){ return spareInTrunk; }
    void setSpare( const Wheel &newSpare ){ spareInTrunk = newSpare; }
    // There are getters and setters for the other wheels too,
    //but they aren't important for this example
};

Тогда я увижу функцию, подобную этой:

void wheelsRelatedToCar( Car *aCar, list< Wheel > &wheels )
{
    aCar->wheelsOnCar( wheels );
    wheels.push_back( aCar->getSpare() );
}

Это реальный пример с именами классов и функций, которые, конечно, были изменены. Почему кто-то хочет, чтобы wheelsRelatedToCar не был членом функции Car? В этом реальном примере Car и Wheel были в одной библиотеке. Глобальная функция была определена в исходном файле в конкретном приложении, использующем эту библиотеку, поэтому был выдвинут аргумент, что функция специфична для приложения. Мой ответ состоял в том, что это была совершенно законная операция на автомобиле и принадлежала к классу автомобилей. Есть ли другая точка зрения на это (кроме тех, кто не предпочитает использовать объектно-ориентированный дизайн)?

Ответы [ 10 ]

21 голосов
/ 28 октября 2009

Скотт Мейерс отстаивает, что функции, не являющиеся членами, часто улучшают инкапсуляцию:

Херб Саттер и Джим Хислоп также говорят об этом (цитируя статью Мейера) в "Самодостаточных заголовках"

Эти идеи были переизданы (в более изящной форме) в 3-м издании «Эффективного C ++» Мейера , «Пункт 23: Предпочитать функции, не являющиеся членами, не являющимися друзьями, функциям-членам» и Саттер / Александреску "Стандарты кодирования C ++" , "44 - Предпочитают писать функции, не являющиеся членами".

Я думаю, что многие разработчики находят это не интуитивным и, возможно, немного спорным.

20 голосов
/ 28 октября 2009

Рекомендации Херба Саттера и Андрея Александреску:

Избегайте членских взносов: по возможности, предпочитайте, чтобы функции были не членами.


Функции, не являющиеся членами группы:

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

Теперь, чтобы ответить на ваш вопрос ( когда? ), вот алгоритм для определения, должна ли функция быть членом и / или другом:

If the function is one of the operators =, ->, [], or (), which must be members:
=> Make it a member

Else if: 
    a) the function needs a different type as its left-hand argument (as do stream operators, for example); 
 or b) it needs type conversions on its leftmost argument; 
 or c) it can be implemented using the class's public interface alone:
=> Make it a nonmember (and friend if needed in cases a) and b) )

If it needs to behave virtually:
=> Add a virtual member function to provide the virtual behavior, and implement the nonmember in terms of that

Else: 
=> Make it a member

Ссылки

  • H. Саттер и Андрей Александреску . Стандарты кодирования C ++ (Addison-Wesley, 2004)
  • S. Мейерс . «Как функции, не являющиеся членами, улучшают инкапсуляцию» (C / C ++ Users Journal, 18 (2), февраль 2000 г.)
  • B.Stroustrup . Язык программирования C ++ (Special 3rdEdition) (Addison-Wesley, 2000). §10.3.2, §11.3.2, §11.3.5, §11.5.2, §21.2.3.1
  • H. Саттер . Исключительный C ++ (Addison-Wesley, 2000).
  • H. Саттер . Исключительный стиль C ++ (Addison-Wesley, 2004).
13 голосов
/ 28 октября 2009

OK

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

Ваш ответ корректен, потому что проще создавать обобщенные методы, если они не являются частью класса (например, почему std :: find не является членом std :: vector? Потому что тогда он не будет работать для списков, карты и т. д.).

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

Когда вы выйдете в реальный мир и начнете писать приложения и будете работать в сжатые сроки, вы увидите, что каждое приложение представляет собой конкурирующий набор требований. «Мы должны получить A, B и C, но к концу следующего месяца. Это невозможно, они могут иметь B & C или A & C, но не A & B. Или они могут не иметь идеальная версия A & B и хорошая версия C ".

Написание кода ничем не отличается, есть множество законов и правил, которые определяют идеальные уровни инкапсуляции, универсальности, сплоченности и т. Д., Но многие из них противоречивы, и вы тратите так много времени, пытаясь удовлетворить их все, что вы ничего не получаете сделано.

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

Надеюсь, это поможет.

12 голосов
/ 28 октября 2009

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

5 голосов
/ 28 октября 2009

Один из способов взглянуть на это так:

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

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

3 голосов
/ 10 ноября 2012

В дополнение к уже упомянутым статьям, думаю, стоит процитировать несколько дополнительных мнений экспертов:


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


  • Бьярне Страуструп. Язык программирования C ++:

10.3.2 Вспомогательные функции

Как правило, класс имеет ряд связанных с ним функций, которые не нужно определять в классе сам, потому что им не нужен прямой доступ к представительству. Например:

int diff(Date a,Date b); // number of days in the range [a,b) or [b,a)
bool leapyear(int y);
Date next_weekday(Date d);
Date next_saturday(Date d);

Определение таких функций в самом классе усложнило бы интерфейс класса и увеличило бы количество функций, которые потенциально необходимо было бы изучить, когда рассматривалось изменение представления . Как такие функции «связаны» с классом Date? Традиционно их заявления были просто помещаются в тот же файл, что и объявление класса Date, и пользователи, которым нужны Dates, делают их доступными, включая файл, определяющий интерфейс.


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

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

Хотя я согласен с большинством других ответов здесь, что бесплатные функции следует использовать, когда это возможно, но есть и другая сторона медали, которая требует внимания: взгляд прагматичного программиста. Если я новичок в коде базы, обнаруживаемость поведения для данного типа будет бесконечно выше, если это поведение отображается как функции-члены. Подумайте intellisense . Вы не получите этого с бесплатными функциями.

1 голос
/ 28 октября 2009

Ваш коллега прав. wheelsRelatedToCar не должно быть функцией-членом class Car, потому что эта операция даже не применяется (только) к автомобилю. Эта функция что-то делает с Car и что-то еще со списком колес.

Таким образом, это должна быть функция-член некоторого другого класса более высокого уровня, такого как class Mechanic или class WheelReplacementOperation. И в случае, если у вас нет такого класса (хотя я думаю, что вы должны иметь!), Лучше сделайте эту функцию глобальной.

0 голосов
/ 28 октября 2009

Итак, вы говорите, что строка не должна иметь публичного члена "bool IsEmpty() const" только потому, что она имеет "int Size() const", а IsEmpty определен и реализован как Size() == 0?

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

0 голосов
/ 28 октября 2009

Часть решения сделать что-либо членом или не членом также связана со сложностью и сложностью управления.

Например, если существует 20 или меньше функций, которые необходимо связать с классом, то почему бы не сделать их всеми функциями-членами.

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...