Używanie klas znajomych do enkapsulacji funkcji prywatnych członków w C ++ - dobra praktyka czy nadużycie?

12

Zauważyłem więc, że można uniknąć umieszczania funkcji prywatnych w nagłówkach, wykonując coś takiego:

// 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);
    }

Funkcja prywatna nigdy nie jest deklarowana w nagłówku, a konsumenci klasy, która importuje nagłówek, nigdy nie muszą wiedzieć, że istnieje. Jest to wymagane, jeśli funkcja pomocnika jest szablonem (alternatywnie jest umieszczenie pełnego kodu w nagłówku), i tak to „odkryłem”. Kolejną zaletą nie jest konieczność ponownej kompilacji każdego pliku zawierającego nagłówek, jeśli dodasz / usuniesz / zmodyfikujesz funkcję prywatnego członka. Wszystkie funkcje prywatne znajdują się w pliku .cpp.

Więc...

  1. Czy to dobrze znany wzorzec projektowy, od którego pochodzi nazwa?
  2. Dla mnie (pochodzący z Java / C # i uczący się C ++ na swój własny czas) wydaje się to bardzo dobrą rzeczą, ponieważ nagłówek definiuje interfejs, podczas gdy .cpp definiuje implementację (a poprawiony czas kompilacji to niezły bonus). Pachnie jednak także, jakby nadużywał funkcji językowej nieprzeznaczonej do użycia w ten sposób. Więc co to jest? Czy to coś, co byś się skrzywił w profesjonalnym projekcie C ++?
  3. Jakieś pułapki, o których nie myślę?

Mam świadomość Pimpl, który jest znacznie bardziej niezawodnym sposobem ukrywania implementacji na krawędzi biblioteki. Jest to bardziej przydatne w przypadku klas wewnętrznych, w których Pimpl spowodowałby problemy z wydajnością lub nie działał, ponieważ klasa powinna być traktowana jako wartość.


EDYCJA 2: Doskonała odpowiedź Dragon Energy poniżej sugeruje następujące rozwiązanie, które w ogóle nie używa friendsłowa kluczowego:

// 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);
    }

Pozwala to uniknąć współczynnika szoku friend(który wydaje się podobny do demonizacji goto) przy jednoczesnym zachowaniu tej samej zasady separacji.

Robert Fraser
źródło
2
Konsument może zdefiniować własną klasę PredicateList_HelperFunctions i pozwolić im uzyskać dostęp do pól prywatnych. ” Czy nie byłoby to naruszenie ODR ? Zarówno ty, jak i konsument musielibyście zdefiniować tę samą klasę. Jeśli te definicje nie są równe, kod jest źle sformułowany.
Nicol Bolas,

Odpowiedzi:

13

To trochę ezoteryczne co najmniej, jak już zauważyłeś, co może powodować, że drapię się po głowie, gdy zaczynam napotykać twój kod, zastanawiając się, co robisz i gdzie te klasy pomocnicze są wdrażane, dopóki nie zacznę wybierać twojego stylu / nawyki (w tym momencie mogę się do tego całkowicie przyzwyczaić).

Podoba mi się, że zmniejszasz ilość informacji w nagłówkach. Zwłaszcza w bardzo dużych bazach kodowych, które mogą mieć praktyczne skutki w celu zmniejszenia zależności od czasu kompilacji i ostatecznie czasu kompilacji.

Moją reakcją jest jednak to, że jeśli czujesz potrzebę ukrywania szczegółów implementacji w ten sposób, aby faworyzować przekazywanie parametrów do funkcji wolnostojących z wewnętrznym łączeniem w pliku źródłowym. Zwykle możesz zaimplementować funkcje narzędziowe (lub całe klasy) przydatne do zaimplementowania określonej klasy bez dostępu do wszystkich elementów wewnętrznych klasy i zamiast tego po prostu przekazać odpowiednie z implementacji metody do funkcji (lub konstruktora). Oczywiście ma to tę zaletę, że zmniejsza sprzężenie między klasą a „pomocnikami”. Ma również tendencję do uogólniania, co w innym przypadku mogłoby być „pomocnikami”, jeśli okaże się, że zaczynają one służyć bardziej ogólnemu celowi, stosowanemu w więcej niż jednej implementacji klasy.

Czasem też trochę się denerwuję, gdy widzę w kodzie mnóstwo „pomocników”. Nie zawsze jest to prawdą, ale czasami mogą być symptomatyczne dla programisty, który po prostu rozkłada funkcje chcąc nie chcąc, aby wyeliminować powielanie kodu z ogromnymi plamami danych przekazywanymi do funkcji o ledwo zrozumiałych nazwach / celach poza faktem, że zmniejszają one ilość kod wymagany do implementacji niektórych innych funkcji. Tylko odrobinę więcej przemyślenia może czasami prowadzić do znacznie większej przejrzystości, jeśli chodzi o sposób, w jaki implementacja klasy jest rozkładana na dalsze funkcje, i faworyzowanie przekazywania określonych parametrów do przekazywania całych instancji obiektu z pełnym dostępem do elementów wewnętrznych może pomóc promować ten styl myślenia projektowego. Oczywiście nie sugeruję, że to robisz (nie mam pojęcia),

Jeśli stanie się to niewygodne, rozważę drugie, bardziej idiomatyczne rozwiązanie, którym jest pimpl (zdaję sobie sprawę, że wspomniałeś o problemach, ale myślę, że możesz uogólnić rozwiązanie, aby uniknąć tych przy minimalnym wysiłku). Może to przenieść wiele informacji, które Twoja klasa musi wdrożyć, w tym prywatne dane, od hurtowego nagłówka. Problemy z wydajnością pimpl można w dużej mierze złagodzić za pomocą taniego taniego alokatora czasu stałego *, takiego jak darmowa lista, przy jednoczesnym zachowaniu semantyki wartości bez konieczności implementowania pełnej definicji ctor kopiowania zdefiniowanej przez użytkownika.

  • Jeśli chodzi o wydajność, pimpl wprowadza co najmniej wskaźnik nad głową, ale myślę, że przypadki muszą być dość poważne, gdy stanowi praktyczną troskę. Jeśli położenie przestrzenne nie ulegnie znacznej degradacji za pomocą alokatora, wówczas ciasne pętle iterujące po obiekcie (które powinny być na ogół jednorodne, jeśli wydajność jest bardzo istotna) nadal będą miały tendencję do minimalizowania braków pamięci podręcznej w praktyce, pod warunkiem, że użyjesz czegoś takiego darmowa lista do przydzielenia pimpl, umieszczająca pola klasy w przeważnie ciągłych blokach pamięci.

Osobiście dopiero po wyczerpaniu tych możliwości rozważyłbym coś takiego. Myślę, że to dobry pomysł, jeśli alternatywa jest jak bardziej prywatne metody narażone na nagłówek, a być może tylko ezoteryczny charakter jest praktyczną troską.

Alternatywa

Jedna alternatywa, która właśnie pojawiła się w mojej głowie, która w dużej mierze osiąga te same cele, gdy nieobecni przyjaciele są następująca:

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);
}

To może wydawać się bardzo sporą różnicą i nadal nazwałbym to „pomocnikiem” (w potencjalnie uwłaczającym sensie, ponieważ wciąż przekazujemy cały wewnętrzny stan klasy do funkcji, czy potrzebuje wszystkiego, czy nie) z wyjątkiem tego, że pozwala uniknąć czynnika „szoku” friend. Zasadniczo friendwygląda to trochę przerażająco, gdy często nieobecna jest dalsza inspekcja, ponieważ mówi, że wewnętrzne elementy klasy są dostępne gdzie indziej (co niesie ze sobą sugestię, że może nie być w stanie utrzymać własnych niezmienników). Sposób, w jaki korzystasz friend, staje się raczej dyskusyjny, jeśli ludzie są świadomi tej praktyki od czasufriendpo prostu znajduje się w tym samym pliku źródłowym, pomagając wdrożyć prywatną funkcjonalność klasy, ale powyższe osiąga ten sam efekt, przynajmniej z jedną możliwą sporną korzyścią, że nie angażuje żadnych przyjaciół, którzy unikają tego rodzaju ( strzelać, ta klasa ma przyjaciela. Skąd jeszcze można uzyskać dostęp do mutacji? ”). Podczas gdy bezpośrednio powyższa wersja natychmiast informuje, że nie ma możliwości uzyskania dostępu / zmutowania szeregów prywatnych przed wszystkim, co zrobiono w trakcie wdrażania PredicateList.

Być może zmierza to w kierunku nieco dogmatycznych terytoriów z tym poziomem niuansów, ponieważ każdy może szybko dowiedzieć się, czy jednolicie nazywasz rzeczy *Helper*i umieszczasz je wszystkie w tym samym pliku źródłowym, który jest zgrupowany razem jako część prywatnej implementacji klasy. Ale jeśli zrobimy się wybredni, być może natychmiastowy styl nie spowoduje tak gwałtownej reakcji na kolano na pierwszy rzut oka, bez friendsłowa kluczowego, które wydaje się nieco przerażające.

W przypadku pozostałych pytań:

Konsument może zdefiniować własną klasę PredicateList_HelperFunctions i pozwolić im uzyskać dostęp do pól prywatnych. Chociaż nie uważam tego za ogromny problem (jeśli naprawdę chciałeś na tych prywatnych polach, możesz zrobić casting), może zachęciłoby to konsumentów do korzystania w ten sposób?

Może to być możliwe ponad granicami API, w których klient mógłby zdefiniować drugą klasę o tej samej nazwie i uzyskać w ten sposób dostęp do elementów wewnętrznych bez błędów łączenia. Z drugiej strony jestem w dużej mierze koderem C pracującym w grafice, w którym bezpieczeństwo na tym poziomie „co jeśli” jest bardzo niskie na liście priorytetów, więc takie obawy to tylko te, na które zwykle macham rękami i tańczę i próbuj udawać, że nie istnieją. :-D Jeśli pracujesz w domenie, w której takie obawy są dość poważne, myślę, że warto wziąć to pod uwagę.

Powyższa alternatywna propozycja pozwala również uniknąć tego problemu. Jeśli jednak nadal chcesz używać friend, możesz również uniknąć tego problemu, zmieniając pomocnika w prywatną klasę zagnieżdżoną.

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

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

Czy to dobrze znany wzorzec projektowy, od którego pochodzi nazwa?

Według mojej wiedzy żaden. Wątpię, by istniał, ponieważ naprawdę zagłębia się w szczegóły szczegółów i stylu implementacji.

„Helper Hell”

Otrzymałem prośbę o dodatkowe wyjaśnienie na temat tego, jak czasami się mylę, gdy widzę implementacje z dużą ilością „pomocniczego” kodu, i może to być nieco kontrowersyjne z niektórymi, ale w rzeczywistości jest to faktyczne, ponieważ tak naprawdę zrobiłem cringe, gdy debugowałem niektóre wdrożenia klasy przez moich kolegów tylko po to, by znaleźć mnóstwo „pomocników”. :-D Nie byłem jedynym w zespole, który drapał się po głowie, próbując dowiedzieć się, co dokładnie powinni zrobić wszyscy ci pomocnicy. Nie chcę też odejść od dogmatyki, takiej jak: „Nie powinieneś używać pomocników”, ale dałbym małą sugestię, że może pomóc pomyśleć o tym, jak wdrożyć rzeczy nieobecne, gdy jest to praktyczne.

Czy wszystkie prywatne funkcje członka nie są funkcjami pomocniczymi z definicji?

I tak, uwzględniam metody prywatne. Jeśli widzę klasę z prostym publicznym interfejsem, ale jak niekończący się zestaw prywatnych metod, które są nieco źle zdefiniowane w celu jak find_impllub find_detaillub find_helper, to również kulę się w podobny sposób.

Jako alternatywę sugeruję nieprzyjazne funkcje niepowiązane z wewnętrznym łączeniem (zadeklarowane staticlub wewnątrz anonimowej przestrzeni nazw), aby pomóc wdrożyć klasę w co najmniej bardziej ogólnym celu niż „funkcja, która pomaga implementować inne”. I mogę tu przytoczyć Herb Sutter z C ++ „Kodowanie standardów”, dlaczego może to być lepsze z ogólnego punktu widzenia SE:

Unikaj opłat członkowskich: tam, gdzie to możliwe, wolą robić funkcje nieprzyjazne członkom. [...] Nieprzyjazne funkcje nieprzyjazne poprawiają enkapsulację poprzez minimalizowanie zależności: Ciało funkcji nie może zależeć od niepublicznych członków klasy (patrz punkt 11). Rozbijają także klasy monolityczne, aby uwolnić rozdzielną funkcjonalność, co dodatkowo zmniejsza sprzężenie (patrz pozycja 33).

Można również zrozumieć „opłaty członkowskie”, o których mówi w pewnym stopniu, w odniesieniu do podstawowej zasady zawężania zakresu zmiennego. Jeśli wyobrażasz sobie, jako najbardziej skrajny przykład, obiekt Boga, który ma cały kod wymagany do uruchomienia całego programu, faworyzuj tego rodzaju „pomocników” (funkcje, członków lub znajomych), którzy mogą uzyskać dostęp do wszystkich elementów wewnętrznych ( privates) klasy w zasadzie czynią te zmienne nie mniej problematycznymi niż zmienne globalne. Masz wszystkie trudności z prawidłowym zarządzaniem stanem i bezpieczeństwem wątków oraz utrzymywaniem niezmienników, które można uzyskać za pomocą zmiennych globalnych w tym najbardziej ekstremalnym przykładzie. I oczywiście, większość prawdziwych przykładów nie jest tak blisko tego ekstremum, ale ukrywanie informacji jest tylko tak przydatne, ponieważ ogranicza zakres dostępnych informacji.

Teraz Sutter podaje tutaj dobre wyjaśnienie, ale dodam też dalej, że oddzielenie ma tendencję do promowania jak poprawa psychologiczna (przynajmniej jeśli twój mózg działa jak mój) pod względem sposobu projektowania funkcji. Kiedy zaczynasz projektować funkcje, które nie mogą uzyskać dostępu do wszystkiego w klasie, z wyjątkiem tylko odpowiednich parametrów, które przekazujesz lub, jeśli przekazujesz instancję klasy jako parametr, tylko jej publiczne elementy, ma ona tendencję do promowania sposobu myślenia, który faworyzuje funkcje, które mają bardziej przejrzysty cel, oprócz oddzielania i promowania ulepszonego hermetyzacji, niż to, co w innym przypadku można by skusić zaprojektować, gdybyś miał dostęp do wszystkiego.

Jeśli wrócimy do skrajności, baza kodów pełna zmiennych globalnych nie kusi programistów do projektowania funkcji w sposób jasny i uogólniony. Bardzo szybko im więcej informacji można uzyskać w funkcji, tym bardziej wielu z nas śmiertelników staje w obliczu pokusy zdegenerowania jej i zmniejszenia jej przejrzystości na korzyść dostępu do wszystkich tych dodatkowych informacji, które mamy zamiast akceptować bardziej szczegółowe i odpowiednie parametry dla tej funkcji zawęzić dostęp do państwa i rozszerzyć jego zastosowanie oraz poprawić jasność intencji. Dotyczy to (choć ogólnie w nieco mniejszym stopniu) funkcji członków lub znajomych.

Dragon Energy
źródło
1
Dzięki za wkład! Nie do końca rozumiem, skąd pochodzisz z tej części: „Czasem też trochę się denerwuję, gdy widzę w kodzie mnóstwo„ pomocników ”. - Czy wszystkie prywatne funkcje członka nie są funkcjami pomocniczymi z definicji? Wydaje się, że ogólnie ma to problem z prywatnymi funkcjami członka.
Robert Fraser,
1
Ach, wewnętrzna klasa nie potrzebuje „przyjaciel” w ogóle, więc robi to w ten sposób całkowicie unika słowa kluczowego „przyjaciel”
Robert Fraser
„Czy wszystkie funkcje pomocnicze prywatnych członków nie są z definicji funkcjami? Wydaje się, że ogólnie dotyczy to prywatnych funkcji członkowskich”. To nie jest największa rzecz. Kiedyś myślałem, że praktyczną koniecznością jest to, że w przypadku nietrywialnej implementacji klasy masz albo szereg funkcji prywatnych, albo pomocników mających dostęp do wszystkich członków klasy na raz. Ale spojrzałem na styl niektórych wielkich, takich jak Linus Torvalds, John Carmack, i chociaż poprzednie kody w C, kiedy koduje analogiczny odpowiednik obiektu, udaje mu się ogólnie zakodować go żadnym z nich razem z Carmackiem.
Dragon Energy
I naturalnie myślę, że pomocnicy w pliku źródłowym są lepsi od jakiegoś ogromnego nagłówka, który zawiera znacznie więcej zewnętrznych nagłówków niż to konieczne, ponieważ używał wielu prywatnych funkcji do pomocy w implementacji klasy. Ale po przestudiowaniu stylu powyższych i innych, zdałem sobie sprawę, że często jest możliwe napisanie funkcji, które są nieco bardziej uogólnione niż typy, które potrzebują dostępu do wszystkich wewnętrznych elementów klasy, nawet po to, aby zaimplementować tylko jedną klasę. dobrze nazwać tę funkcję i przekazać jej konkretnym członkom, których potrzebuje często, w efekcie oszczędza więcej czasu [...]
Dragon Energy
[...] niż potrzeba, co daje bardziej przejrzystą ogólną implementację, którą łatwiej można później modyfikować. To tak, jakby zamiast pisać „predykat pomocnika” dla „pełnego dopasowania”, który uzyskuje dostęp do wszystkiego na twoim koncie PredicateList, często może być po prostu przekazanie członka lub dwóch z listy predykatów do nieco bardziej ogólnej funkcji, która nie potrzebuje dostępu do każdy członek prywatny PredicateList, i często taki, będzie miał tendencję do nadawania bardziej przejrzystej, bardziej ogólnej nazwy i celu tej funkcji wewnętrznej, a także więcej możliwości „ponownego wykorzystania kodu z perspektywy czasu”.
Dragon Energy