Використання друзів класів для інкапсуляції функцій приватних членів на 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 # background і навчається C ++ самостійно), це здається дуже хорошою справою, оскільки заголовок визначає інтерфейс, тоді як .cpp визначає реалізацію (а покращений час компіляції - це приємний бонус). Однак це також пахне тим, що це зловживання мовною функцією, не призначеною для використання таким чином. Отже, що це? Це щось, що ви б нахмурилися побачити в професійному проекті C ++?
  3. Про якісь підводні камені, про які я не думаю?

Я знаю про Pimpl, який є набагато більш надійним способом приховування реалізації на межі бібліотеки. Це більше для використання з внутрішніми класами, де Pimpl може спричинити проблеми з роботою або не працювати, тому що клас слід розглядати як значення.


EDIT 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 (я розумію, ви з цим згадали проблеми, але, думаю, ви можете узагальнити рішення, щоб уникнути тих, хто докладе мінімум зусиль). Це може перемістити багато інформації, яку ваш клас потребує впровадити, включаючи його приватні дані далеко від заголовка. Проблеми продуктивності pimpl можна значною мірою пом'якшити за допомогою брудового розподільника постійного часу *, як безкоштовного списку, зберігаючи семантику значень без необхідності впровадження повномасштабного визначеного користувачем копіюючого копію.

  • Для аспекту продуктивності 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або всередині анонімного простору імен), щоб допомогти реалізувати свій клас принаймні більш узагальненою метою, ніж "функція, яка допомагає реалізувати інші". І я можу навести тут Herb Sutter з C ++ «Стандартів кодування», чому це може бути кращим із загальної точки зору SE:

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

Ви також можете зрозуміти "членські внески", про які він говорить певною мірою з точки зору основного принципу звуження змінної сфери. Якщо ви уявляєте, як самий крайній приклад, об’єкт «Бог», у якого є весь код, необхідний для запуску всієї вашої програми, то надайте перевагу «таким помічникам» (функціям, чи то членам, чи друзям), які можуть отримати доступ до всіх внутрішніх справ ( приватні особи) класу в основному роблять ці змінні не менш проблемними, ніж глобальні змінні. У вас є всі труднощі з правильним керуванням державою та безпекою потоку та підтримкою інваріантів, які ви отримаєте із глобальними змінними в цьому найвиразнішому прикладі. І звичайно, більшість реальних прикладів, сподіваємось, не є десь близькими до цієї крайності, але приховування інформації є настільки ж корисним, як і обмеження обсягу інформації, до якої можна отримати доступ.

Тепер Саттер вже дає приємне пояснення тут, але я також додам, що розв'язка має тенденцію сприяти психологічному поліпшенню (принаймні, якщо ваш мозок працює як мій) з точки зору того, як ви проектуєте функції. Коли ви починаєте проектувати функції, які не можуть отримати доступ до всього класу, за винятком лише відповідних параметрів, які ви передаєте, або, якщо ви передаєте екземпляр класу як параметр, лише його загальнодоступним членам, він прагне сприяти дизайнерському мисленню, що сприяє функції, які мають більш чітке призначення, крім роз’єднання та сприяння вдосконаленому капсулуванню, ніж те, що ви в іншому випадку могли б спокусити розробити, якби ви могли просто отримати доступ до всього.

Якщо ми повернемося до кінцівок, то база коду, пронизана глобальними змінними, не точно спокушає розробників проектувати функції зрозумілим та узагальненим за призначенням. Дуже швидко, чим більше інформації ви можете отримати в функції, тим більшість із нас смертних стикаються з спокусою її знеструмити і зменшити її чіткість на користь доступу до всієї цієї додаткової інформації, яку ми маємо замість прийняття більш конкретних і релевантних параметрів для цієї функції. звузити доступ до держави та розширити його застосовність та покращити ясність намірів. Це стосується (хоча взагалі в меншій мірі) функцій членів або друзів.


1
Дякую за вклад! Я не зовсім розумію, звідки ви беретеся з цією частиною, хоча: "Я також іноді трохи стискаюся, коли бачу в коді багато" помічників "." - Чи не всі приватні члени є помічниками функцій за визначенням? Це, мабуть, викликає проблеми з функціями приватних членів взагалі.
Роберт Фрейзер

1
Ах, внутрішній клас взагалі не потребує "друга", тому таким чином повністю уникає ключового слова "друг"
Роберт Фрейзер

"Чи не всі функції приватних членів помічники функцій за визначенням? Це, здається, взагалі викликає проблеми з функціями приватного члена." Це не найбільше. Раніше я вважав, що це практична необхідність, що для нетривіальної реалізації класу ви маєте або кілька приватних функцій, або помічників, які мають доступ до всіх членів класу одночасно. Але я подивився на стиль деяких великих людей, таких як Лінус Торвальдс, Джон Кармак, і хоча колишні коди в С, коли він кодує аналогічний еквівалент об'єкта, йому вдається загалом кодувати його ні разом із Кармаком.
Драконова енергія

І, звичайно, я вважаю, що помічники у вихідному файлі є кращими до масивного заголовка, який включає в себе набагато більше зовнішніх заголовків, ніж необхідно, оскільки він використовував багато приватних функцій, щоб допомогти реалізувати клас. Але вивчивши стиль вищезгаданих та інших, я зрозумів, що часто можна записати функції, які є трохи більш узагальненими, ніж типи, яким потрібен доступ до всіх внутрішніх членів класу навіть для простого впровадження одного класу, і думка наперед щоб добре назвати функцію та передати її певним членам, які їй потрібні для роботи, в кінцевому рахунку економиться більше часу [...]
енергія Дракона

[...] ніж це потрібно, що дає чіткішу реалізацію загалом, що згодом простіше маніпулювати. Це як замість того, щоб писати "допоміжний предикат" для "повного матчу", який отримує доступ до всього, що є у вашому PredicateList, часто може бути можливим просто передати члена або двох зі списку предикатів на дещо більш узагальнену функцію, яка не потребує доступу до кожен приватний член PredicateList, і часто це, як правило, також дає більш чітке, узагальнене ім’я та призначення цієї внутрішньої функції, а також більше можливостей для "повторного використання коду заднього огляду".
Енергія Дракона
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.