Использование классов-друзей для инкапсуляции закрытых функций-членов в C ++ - хорошая практика или злоупотребление?

12

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

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

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

Так...

  1. Это хорошо известный шаблон дизайна, название которого существует?
  2. Для меня (исходя из фона Java / C # и изучения C ++ в свое свободное время), это кажется очень хорошей вещью, поскольку заголовок определяет интерфейс, а .cpp определяет реализацию (а улучшенное время компиляции приятный бонус). Тем не менее, он также пахнет тем, что злоупотребляет языковой функцией, не предназначенной для такого использования. Итак, что это? Это то, что вы хотели бы увидеть в профессиональном проекте C ++?
  3. Какие-нибудь подводные камни, о которых я не думаю?

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


РЕДАКТИРОВАТЬ 2: превосходный ответ Dragon Energy ниже предложил следующее решение, которое не использует friendключевое слово вообще:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

Это позволяет избежать шокового фактора friend(который, похоже, был демонизирован как goto), при этом сохраняя тот же принцип разделения.

Роберт Фрейзер
источник
2
« Потребитель может определить свой собственный класс PredicateList_HelperFunctions и разрешить ему доступ к закрытым полям. » Разве это не было бы нарушением ODR ? И вы, и потребитель должны были бы определять один и тот же класс. Если эти определения не равны, то код является неправильным.
Николь Болас

Ответы:

13

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

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

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

Я также иногда немного съеживаюсь, когда вижу много «помощников» в коде. Это не всегда так, но иногда они могут быть симптомом для разработчика, который просто произвольно декомпозирует функции, чтобы устранить дублирование кода с огромными каплями данных, передаваемых функциям с едва понятными именами / целями, за исключением того факта, что они уменьшают количество код, необходимый для реализации некоторых других функций. Чуть-чуть более продуманный заранее может иногда привести к гораздо большей ясности с точки зрения того, как реализация класса разбивается на дополнительные функции, и может помочь передача определенных параметров для передачи целых экземпляров вашего объекта с полным доступом к внутренним компонентам. продвигать этот стиль дизайна мысли. Я не предполагаю, что вы делаете это, конечно (я понятия не имею),

Если это станет громоздким, я бы рассмотрел второе, более идиоматическое решение, которое является прыщом (я понимаю, что вы упомянули проблемы с ним, но я думаю, что вы можете обобщить решение, чтобы избежать их с минимальными усилиями). Это может отодвинуть всю информацию, которую необходимо реализовать вашему классу, включая его личные данные, из оптовых заголовков. Проблемы производительности pimpl могут быть в значительной степени устранены с помощью дешевого распределителя постоянного времени *, такого как свободный список, при сохранении семантики значений без необходимости реализации полноценного пользовательского копирования ctor.

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

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

Альтернатива

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

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

Теперь это может показаться очень спорным отличием, и я все равно буду называть его «помощником» (возможно, в уничижительном смысле, поскольку мы все еще передаем функции все внутреннее состояние класса независимо от того, нужно ему все это или нет) за исключением того, что он избегает «шокового» фактора столкновения friend. В общем, friendвыглядит немного страшно, что часто отсутствует дополнительная проверка, поскольку в нем говорится, что внутренние компоненты вашего класса доступны в другом месте (что подразумевает, что он не может поддерживать свои собственные инварианты). С тем, как вы используете, friendстановится довольно спорным, если люди знают о практике, так какfriendпросто находится в одном и том же исходном файле, помогая реализовать приватную функциональность класса, но вышеприведенное обеспечивает практически тот же эффект, по крайней мере, с одним возможным спорным преимуществом, заключающимся в том, что в нем не участвуют друзья, избегающие такого рода («О блин, у этого класса есть друг. Куда еще его рядовые получают доступ / видоизменяются? "). Принимая во внимание, что вышеприведенная версия немедленно сообщает о том, что для рядовых не может быть доступа / мутирования вне всего, что сделано в реализации PredicateList.

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

По другим вопросам:

Потребитель может определить свой собственный класс PredicateList_HelperFunctions и разрешить ему доступ к закрытым полям. Хотя я не рассматриваю это как огромную проблему (если вы действительно хотите на этих частных полях, вы могли бы провести кастинг), возможно, это побудит потребителей использовать его таким образом?

Это может быть возможностью через границы API, когда клиент может определить второй класс с тем же именем и получить доступ к внутренним компонентам таким образом без ошибок компоновки. С другой стороны, я в основном программист на C, работающий в графике, где проблемы безопасности на этом уровне «что если» очень низки в списке приоритетов, поэтому такие проблемы, как эти, - это те, на которые я склонен махать руками и танцевать и попытаться притвориться, будто их не существует. :-D Если вы работаете в области, где подобные проблемы довольно серьезны, я думаю, это достойное рассмотрение.

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

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

Это хорошо известный шаблон дизайна, название которого существует?

Ни один, насколько мне известно. Я в некотором роде сомневаюсь, что он будет, поскольку он действительно вдавается в мелочи деталей и стиля реализации.

"Хелпер Ад"

Я получил запрос на дополнительные разъяснения относительно точки о том , как я иногда передергивает , когда я вижу реализации с большим количеством «хелпера» кода, и это может быть немного спорным с некоторыми , но это на самом деле фактическая , как я действительно съежиться , когда я отладки некоторых реализации моих коллег только для того, чтобы найти множество «помощников». :-D И я был не единственным в команде, почесывая голову, пытаясь понять, что именно должны делать все эти помощники. Я также не хочу отказываться от догматики типа «Не пользуйся помощниками», но я хотел бы сделать крошечное предположение, что это может помочь подумать о том, как реализовать вещи, которых нет у них, когда это практически возможно.

Разве все частные функции-члены не являются вспомогательными функциями по определению?

И да, я в том числе частные методы. Если я вижу класс с простым общедоступным интерфейсом, но с бесконечным набором частных методов, которые в некоторой степени плохо определены по назначению, например find_implили find_detailили find_helper, то я также искажаю подобным образом.

В качестве альтернативы я предлагаю функции, не являющиеся членами, не являющимися членами, с внутренней связью (объявленной staticили внутри анонимного пространства имен), чтобы помочь реализовать ваш класс по крайней мере с более общей целью, чем «функция, которая помогает реализовать другие». И я могу привести Херба Саттера из C ++ «Стандарты кодирования» здесь, почему это может быть предпочтительнее с общей точки зрения SE:

Избегайте членских взносов: по возможности, предпочитайте, чтобы функции не были членами группы. [...] Функции, не являющиеся членами, не являющимися членами, улучшают инкапсуляцию за счет минимизации зависимостей: тело функции не может зависеть от непубличных членов класса (см. Пункт 11). Они также разбивают монолитные классы, освобождая разделяемую функциональность, дополнительно уменьшая связь (см. Пункт 33).

Вы также можете понять «членские взносы», о которых он говорит в некоторой степени с точки зрения основного принципа сужения переменной сферы действия. Если вы представите, как самый крайний пример, объект God, который имеет весь код, необходимый для запуска всей вашей программы, то предпочтение отдается «помощникам» такого рода (функциям, будь то функции-члены или друзья), которые могут получить доступ ко всем внутренним объектам ( Приват) класса в основном делают эти переменные не менее проблематичными, чем глобальные переменные. У вас есть все трудности, связанные с правильным управлением состоянием, безопасностью потоков и поддержкой инвариантов, которые вы получили бы с помощью глобальных переменных в этом наиболее экстремальном примере. И, конечно, большинство реальных примеров, надеюсь, не близко к этому крайнему значению, но сокрытие информации настолько же полезно, насколько и ограничение объема информации, к которой осуществляется доступ.

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

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

Энергия Дракона
источник
1
Спасибо за вклад! Я не совсем понимаю, откуда вы взялись с этой частью: «Я также иногда немного съеживаюсь, когда вижу много« помощников »в коде». Разве не все частные функции-члены вспомогательные функции по определению? Похоже, что это имеет проблемы с частными функциями-членами в целом.
Роберт Фрейзер
1
Ах, внутренний класс вообще не нуждается в «друге», поэтому при таком подходе полностью исключается ключевое слово «друг»
Роберт Фрейзер,
«Разве не все частные функции-помощники по определению являются вспомогательными функциями? Это не самая большая вещь. Раньше я считал практической необходимостью, чтобы для нетривиальной реализации класса у вас было несколько частных функций или помощников, имеющих доступ ко всем членам класса одновременно. Но я посмотрел на стиль некоторых из великих людей, таких как Линус Торвальдс, Джон Кармак, и, хотя прежние коды в C, когда он кодирует аналогичный эквивалент объекта, ему удается в общем кодировать его ни вместе с Кармаком.
Энергия Дракона,
И, естественно, я думаю, что помощники в исходном файле предпочтительнее какого-то массивного заголовка, который включает в себя гораздо больше внешних заголовков, чем необходимо, потому что он использовал много частных функций, чтобы помочь реализовать класс. Но после изучения стиля вышеупомянутых и других, я понял, что часто можно написать функции, которые являются немного более обобщенными, чем типы, которым нужен доступ ко всем внутренним членам класса, даже для реализации одного класса, и продуманный заранее. правильно называть функцию и передавать ее конкретным членам, которые ей нужны для работы, часто экономит больше времени [...]
Dragon Energy
[...] чем требуется, что в итоге дает более четкую реализацию, которой потом легче манипулировать. Это похоже на то, что вместо написания «предиката помощника» для «полного соответствия», который обращается ко всему в вашем PredicateList, часто может быть целесообразным просто передать один или два члена из списка предикатов в чуть более обобщенную функцию, которая не нуждается в доступе к каждый частный член PredicateList, и часто это будет иметь тенденцию также давать более четкое, более обобщенное имя и цель для этой внутренней функции, а также больше возможностей для «повторного использования кода задним числом».
Энергия Дракона,