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...
- Czy to dobrze znany wzorzec projektowy, od którego pochodzi nazwa?
- 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 ++?
- 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 friend
sł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.
źródło
Odpowiedzi:
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.
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.
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:
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
. Zasadniczofriend
wyglą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 korzystaszfriend
, staje się raczej dyskusyjny, jeśli ludzie są świadomi tej praktyki od czasufriend
po 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żaniaPredicateList
.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, bezfriend
słowa kluczowego, które wydaje się nieco przerażające.W przypadku pozostałych pytań:
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ą.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.
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_impl
lubfind_detail
lubfind_helper
, to również kulę się w podobny sposób.Jako alternatywę sugeruję nieprzyjazne funkcje niepowiązane z wewnętrznym łączeniem (zadeklarowane
static
lub 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: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.
źródło
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 prywatnyPredicateList
, 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”.