Verwenden von Friend-Klassen, um private Member-Funktionen in C ++ zu kapseln - bewährte Methoden oder Missbrauch?

12

Ich bemerkte also, dass es möglich ist, das Einfügen privater Funktionen in Kopfzeilen zu vermeiden, indem man Folgendes ausführt:

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

Die private Funktion wird niemals im Header deklariert, und die Konsumenten der Klasse, die den Header importieren, müssen nie wissen, dass er existiert. Dies ist erforderlich, wenn es sich bei der Hilfsfunktion um eine Vorlage handelt (alternativ wird der vollständige Code in den Header eingefügt). Auf diese Weise habe ich dies "entdeckt". Ein weiterer Vorteil ist, dass Sie nicht jede Datei neu kompilieren müssen, die den Header enthält, wenn Sie eine private Member-Funktion hinzufügen, entfernen oder ändern. Alle privaten Funktionen befinden sich in der CPP-Datei.

So...

  1. Ist das ein bekanntes Designmuster, für das es einen Namen gibt?
  2. Für mich (mit Java / C # -Hintergrund und eigenem C ++ -Lernen) scheint dies eine sehr gute Sache zu sein, da der Header eine Schnittstelle definiert, während die CPP eine Implementierung definiert (und die Kompilierungszeit verbessert wird) ein schöner Bonus). Es riecht jedoch auch, als würde es eine Sprachfunktion missbrauchen, die nicht für diesen Zweck vorgesehen ist. Also, was ist das? Ist das etwas, was Sie in einem professionellen C ++ - Projekt nicht gerne sehen würden?
  3. Irgendwelche Fallstricke, an die ich nicht denke?

Mir ist Pimpl bekannt, eine viel robustere Methode, um die Implementierung am Rande der Bibliothek zu verstecken. Dies ist eher für die Verwendung mit internen Klassen gedacht, bei denen Pimpl Leistungsprobleme verursachen oder nicht funktionieren würde, da die Klasse als Wert behandelt werden muss.


BEARBEITEN 2: Die ausgezeichnete Antwort von Dragon Energy unten schlug die folgende Lösung vor, bei der das friendSchlüsselwort überhaupt nicht verwendet wird :

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

Dies vermeidet den Schockfaktor von friend(der wie dämonisiert zu sein scheint goto), während das gleiche Prinzip der Trennung beibehalten wird.

Robert Fraser
quelle
2
" Ein Verbraucher könnte seine eigene Klasse PredicateList_HelperFunctions definieren und ihn auf die privaten Felder zugreifen lassen. " Wäre das nicht eine ODR-Verletzung ? Sowohl Sie als auch der Verbraucher müssten dieselbe Klasse definieren. Wenn diese Definitionen nicht gleich sind, ist der Code fehlerhaft.
Nicol Bolas

Antworten:

13

Es ist, gelinde gesagt, etwas esoterisch, da Sie bereits erkannt haben, dass ich mir einen Moment lang den Kopf kratzen könnte, als ich zum ersten Mal auf Ihren Code stoße und mich frage, was Sie tun und wo diese Hilfsklassen implementiert sind, bis ich anfange, Ihren Stil aufzunehmen / Gewohnheiten (an diesem Punkt könnte ich mich total daran gewöhnen).

Mir gefällt, dass Sie die Informationsmenge in den Überschriften reduzieren. Insbesondere in sehr großen Codebasen kann dies praktische Auswirkungen haben, um die Abhängigkeiten bei der Kompilierung und letztendlich die Erstellungszeiten zu verringern.

Wenn Sie jedoch das Gefühl haben, Implementierungsdetails auf diese Weise verbergen zu müssen, sollten Sie die Übergabe von Parametern an eigenständige Funktionen mit interner Verknüpfung in der Quelldatei bevorzugen. Normalerweise können Sie Utility-Funktionen (oder ganze Klassen) implementieren, die zum Implementieren einer bestimmten Klasse nützlich sind, ohne Zugriff auf alle Interna der Klasse zu haben. Stattdessen übergeben Sie einfach die relevanten Funktionen von der Implementierung einer Methode an die Funktion (oder den Konstruktor). Und das hat natürlich den Vorteil, die Kopplung zwischen Ihrer Klasse und den "Helfern" zu verringern. Es besteht auch die Tendenz, das zu verallgemeinern, was ansonsten "Helfer" gewesen sein könnte, wenn Sie feststellen, dass sie einem allgemeineren Zweck dienen, der auf mehr als eine Klassenimplementierung anwendbar ist.

Ich erschrecke manchmal auch, wenn ich viele "Helfer" im Code sehe. Es ist nicht immer wahr, aber manchmal können sie symptomatisch für einen Entwickler sein, der Funktionen nur willkürlich zerlegt, um Code-Duplikationen zu vermeiden, wobei riesige Datenblobs an Funktionen mit schwer verständlichen Namen / Zwecken weitergegeben werden, über die Tatsache hinaus, dass sie die Menge von Funktionen reduzieren Code, der zum Implementieren einiger anderer Funktionen erforderlich ist. Nur ein bisschen mehr Überlegung im Voraus kann manchmal zu einer viel größeren Klarheit führen, wie die Implementierung einer Klasse in weitere Funktionen zerlegt wird, und es kann hilfreich sein, bestimmte Parameter an ganze Instanzen Ihres Objekts mit uneingeschränktem Zugriff auf Interna weiterzugeben fördern Sie diesen Stil des Designgedankens. Ich schlage nicht vor, dass Sie das tun, natürlich (ich habe keine Ahnung),

Wenn das unhandlich wird, würde ich eine zweite, idiomatischere Lösung in Betracht ziehen, nämlich den Pimpl (ich weiß, dass Sie Probleme damit angesprochen haben, aber ich denke, Sie können eine Lösung verallgemeinern, um diese mit minimalem Aufwand zu vermeiden). Dies kann eine ganze Reihe von Informationen, die Ihre Klasse implementieren muss, einschließlich ihrer privaten Daten, aus dem Header-Großhandel entfernen. Die Leistungsprobleme der Zuhälter können weitgehend mit einem billigen Zuweiser für konstante Zeit * wie einer freien Liste gemildert werden, während die Wertsemantik beibehalten wird, ohne dass ein vollständiger benutzerdefinierter Kopier-Ctor implementiert werden muss.

  • Für den Performance-Aspekt bringt der Pickel zumindest einen Zeiger-Overhead mit sich, aber ich denke, die Fälle müssen ziemlich ernst sein, wenn sie ein praktisches Problem darstellen. Wenn die räumliche Lokalität durch den Allokator nicht wesentlich verschlechtert wird, werden Ihre engen Schleifen, die über das Objekt iterieren (was im Allgemeinen homogen sein sollte, wenn die Leistung so wichtig ist), in der Praxis immer noch dazu neigen, Cache-Ausfälle zu minimieren, vorausgesetzt, Sie verwenden etwas Ähnliches eine freie Liste zum Zuordnen der Pickel, die die Felder der Klasse in weitgehend zusammenhängende Speicherblöcke einsetzt.

Persönlich würde ich nur nach Ausschöpfung dieser Möglichkeiten so etwas in Betracht ziehen. Ich halte es für eine vernünftige Idee, wenn die Alternative eher privaten Methoden gleicht, die dem Header ausgesetzt sind, wobei möglicherweise nur die esoterische Natur das praktische Problem darstellt.

Eine Alternative

Eine Alternative, die mir gerade in den Sinn gekommen ist und weitgehend dieselben Ziele ohne Freunde erreicht, ist folgende:

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

Nun, das scheint ein sehr strittiger Unterschied zu sein, und ich würde es immer noch als "Helfer" bezeichnen (in einem möglicherweise abfälligen Sinn, da wir immer noch den gesamten internen Zustand der Klasse an die Funktion übergeben, ob sie alles benötigt oder nicht). außer es vermeidet den "Schock" -Faktor der Begegnung friend. Im Allgemeinen friendsieht es ein bisschen beängstigend aus, wenn häufig keine weitere Überprüfung erfolgt, da darauf hingewiesen wird, dass die Interna Ihrer Klasse an anderer Stelle zugänglich sind (was impliziert, dass sie möglicherweise nicht in der Lage ist, ihre eigenen Invarianten aufrechtzuerhalten). Mit der Art, wie Sie es benutzen friend, wird es ziemlich umstritten, wenn die Leute sich der Praxis seit dem bewusst sindfriendbefindet sich nur in der gleichen Quelldatei und hilft, die private Funktionalität der Klasse zu implementieren, aber das oben Gesagte bewirkt fast den gleichen Effekt, zumindest mit dem möglichen Vorteil, dass es keine Freunde mit einbezieht, die diese ganze Art vermeiden ("Oh Schieß, diese Klasse hat einen Freund. Wo sonst werden ihre Privaten abgerufen / mutiert? "). Während die unmittelbar obige Version sofort mitteilt, dass es keine Möglichkeit gibt, auf die Privaten zuzugreifen / sie zu mutieren, ohne dass dies bei der Implementierung von geschehen würde PredicateList.

Das bewegt sich vielleicht in Richtung dogmatischer Gebiete mit dieser Nuance, da jeder schnell herausfinden kann, ob man Dinge einheitlich benennt *Helper* und in derselben Quelldatei ablegen, die sie alle im Rahmen der privaten Implementierung einer Klasse gebündelt haben. Aber wenn wir friendein bisschen wählerisch werden, kann es sein, dass der direkt darüber stehende Stil auf einen Blick nicht zu einer so ruckeligen Reaktion führt, wenn das Schlüsselwort fehlt, das ein bisschen beängstigend wirkt.

Für die anderen Fragen:

Ein Konsument könnte seine eigene Klasse PredicateList_HelperFunctions definieren und ihn auf die privaten Felder zugreifen lassen. Ich sehe dies zwar nicht als großes Problem an (wenn Sie wirklich auf diesen privaten Feldern arbeiten möchten, könnten Sie ein Casting durchführen), aber könnte dies die Verbraucher dazu ermutigen, es auf diese Weise zu verwenden?

Dies könnte eine Möglichkeit über API-Grenzen hinweg sein, bei der der Client eine zweite Klasse mit demselben Namen definieren und auf diese Weise ohne Verknüpfungsfehler Zugriff auf die Interna erhalten könnte. Andererseits bin ich größtenteils ein C-Programmierer, der in Grafikbereichen arbeitet, in denen Sicherheitsbedenken auf dieser Ebene des "Was-wäre-wenn" auf der Prioritätenliste sehr gering sind. Daher neige ich dazu, mit den Händen zu winken und tanzen zu gehen versuche so zu tun, als gäbe es sie nicht. :-D Wenn Sie in einem Bereich arbeiten, in dem solche Bedenken sehr ernst sind, ist dies meines Erachtens eine angemessene Überlegung.

Der obige alternative Vorschlag vermeidet auch, dieses Problem zu erleiden. Wenn Sie trotzdem weitermachen möchten friend, können Sie dieses Problem auch vermeiden, indem Sie den Helfer zu einer privaten verschachtelten Klasse machen.

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

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

Ist das ein bekanntes Designmuster, für das es einen Namen gibt?

Meines Wissens nach keine. Ich bezweifle, dass es eines geben würde, da es wirklich in die Details und den Stil der Implementierung geht.

"Helfer Hölle"

Ich habe eine Bitte um weitere Klärung darüber erhalten, wie ich manchmal zusammenzucken kann, wenn ich Implementierungen mit viel "Helfer" -Code sehe, und das mag bei einigen ein wenig umstritten sein, aber es ist tatsächlich so, wie ich wirklich zusammenzucken musste, als ich einige debuggte der Implementierung einer Klasse durch meine Kollegen, nur um jede Menge "Helfer" zu finden. :-D Und ich war nicht der einzige im Team, der sich am Kopf kratzte, um herauszufinden, was all diese Helfer genau tun sollen. Ich möchte auch nicht dogmatisch abschrecken, wie "Du sollst keine Helfer benutzen", aber ich würde einen winzigen Vorschlag machen, dass es hilfreich sein könnte, darüber nachzudenken, wie man Dinge umsetzt, die nicht in der Praxis vorhanden sind.

Sind nicht per Definition alle privaten Mitgliederfunktionen Hilfsfunktionen?

Und ja, ich beziehe private Methoden ein. Wenn ich eine Klasse mit einer einfachen öffentlichen Schnittstelle sehe, aber mit einer endlosen Menge privater Methoden, deren Zweck wie find_imploder find_detailoder etwas unklar ist find_helper, dann erschaudere ich auch auf ähnliche Weise.

Als Alternative schlage ich Nichtmitgliedsfunktionen mit interner Verknüpfung (deklariert staticoder in einem anonymen Namespace) vor, um die Implementierung Ihrer Klasse mit mindestens einem allgemeineren Zweck als "einer Funktion, die die Implementierung anderer unterstützt" zu unterstützen. Und ich kann Herb Sutter aus C ++ 'Coding Standards' hier zitieren, warum dies unter allgemeinen SE-Gesichtspunkten vorzuziehen ist:

Vermeiden Sie Mitgliedsbeiträge: Wenn möglich, ziehen Sie es vor, Funktionen zu erstellen, die keine Mitglieder von Freunden sind. [...] Nicht-Member-Nicht-Friend-Funktionen verbessern die Kapselung, indem sie Abhängigkeiten minimieren: Der Hauptteil der Funktion kann nicht von den nicht-öffentlichen Mitgliedern der Klasse abhängen (siehe Punkt 11). Sie zerlegen auch monolithische Klassen, um trennbare Funktionen freizusetzen und die Kopplung weiter zu reduzieren (siehe Punkt 33).

Sie können auch die "Mitgliedsbeiträge" verstehen, von denen er in gewissem Maße im Hinblick auf das Grundprinzip der Einschränkung des variablen Umfangs spricht. Wenn Sie sich als extremstes Beispiel ein Gott-Objekt vorstellen, das den gesamten Code enthält, der für die Ausführung Ihres gesamten Programms erforderlich ist, dann bevorzugen Sie "Helfer" dieser Art (Funktionen, ob Mitgliederfunktionen oder Freunde), die auf alle Interna zugreifen können ( privates) einer Klasse machen diese Variablen grundsätzlich nicht weniger problematisch als globale Variablen. Sie haben alle Schwierigkeiten, den Status korrekt zu verwalten, die Sicherheit von Threads zu gewährleisten und Invarianten zu verwalten, die Sie in diesem extremsten Beispiel mit globalen Variablen erhalten würden. Und natürlich sind die meisten realen Beispiele hoffentlich nicht annähernd so extrem, aber das Verbergen von Informationen ist nur so nützlich, wie es den Umfang der Informationen einschränkt, auf die zugegriffen wird.

Jetzt gibt Sutter hier bereits eine nette Erklärung, aber ich möchte noch hinzufügen, dass die Entkopplung dazu neigt, eine psychologische Verbesserung (zumindest wenn Ihr Gehirn so funktioniert wie ich) in Bezug auf Ihre Designfunktionen herbeizuführen. Wenn Sie mit dem Entwerfen von Funktionen beginnen, die nur auf die relevanten Parameter zugreifen können, die Sie übergeben, oder wenn Sie die Instanz der Klasse als Parameter übergeben, nur auf deren öffentliche Mitglieder, tendiert sie dazu, eine Design-Denkweise zu fördern, die bevorzugt Funktionen, die einen klareren Zweck haben, zusätzlich zu der Entkopplung und der Förderung einer verbesserten Kapselung, als das, was Sie sonst möglicherweise zu entwerfen versucht wären, wenn Sie nur auf alles zugreifen könnten.

Wenn wir zu den äußersten Enden zurückkehren, verleitet eine Codebasis mit globalen Variablen Entwickler nicht gerade dazu, Funktionen auf eine Weise zu entwerfen, die klar und allgemein gehalten ist. Sehr schnell, je mehr Informationen Sie in einer Funktion abrufen können, desto mehr von uns Sterblichen sind der Versuchung ausgesetzt, sie zu entarten und ihre Klarheit zu verringern, um auf all diese zusätzlichen Informationen zugreifen zu können, anstatt spezifischere und relevantere Parameter für diese Funktion zu akzeptieren den Zugang zum Staat einzuschränken, die Anwendbarkeit zu erweitern und die Klarheit der Absichten zu verbessern. Dies gilt (wenn auch im Allgemeinen in geringerem Maße) für Mitgliederfunktionen oder Freunde.

Drachen Energie
quelle
1
Danke für die Eingabe! Ich verstehe allerdings nicht ganz, woher du mit diesem Teil kommst: "Ich erschrecke manchmal auch ein bisschen, wenn ich viele" Helfer "im Code sehe." - Sind nicht per Definition alle privaten Mitgliederfunktionen Helferfunktionen? Dies scheint generell ein Problem mit den Funktionen privater Mitglieder zu sein.
Robert Fraser
1
Ah, die innere Klasse braucht überhaupt keinen "Freund". Auf diese Weise wird das Schlüsselwort "Freund"
Robert Fraser,
"Sind nicht alle Funktionen für private Mitglieder per Definition Hilfsfunktionen? Dies scheint generell ein Problem mit Funktionen für private Mitglieder zu sein." Es ist nicht das Größte. Früher hielt ich es für eine praktische Notwendigkeit, dass Sie für eine nicht triviale Klassenimplementierung entweder mehrere private Funktionen oder Helfer mit gleichzeitigem Zugriff auf alle Klassenmitglieder haben. Aber ich habe mir den Stil einiger Größen wie Linus Torvalds, John Carmack angesehen, und obwohl der frühere Code in C das analoge Äquivalent eines Objekts codiert, gelingt es ihm, es zusammen mit Carmack weder mit noch mit zu codieren.
Dragon Energy
Und natürlich denke ich, dass Helfer in der Quelldatei einem massiven Header vorzuziehen sind, der weitaus mehr externe Header als nötig enthält, da viele private Funktionen zur Implementierung der Klasse verwendet wurden. Aber nachdem ich den Stil der oben genannten und anderer untersucht hatte, wurde mir klar, dass es oft möglich ist, Funktionen zu schreiben, die etwas verallgemeinerter sind als die Typen, die Zugriff auf alle internen Mitglieder einer Klasse benötigen, um nur eine Klasse zu implementieren, und den Gedanken im Voraus
Dragon Energy
Sie erhalten eine übersichtlichere Implementierung, die später einfacher zu manipulieren ist. Anstatt ein "Helfer-Prädikat" für "Vollübereinstimmung" zu schreiben, das auf alles in Ihrem System zugreift PredicateList, ist es häufig möglich, nur ein oder zwei Mitglieder aus der Prädikatliste an eine etwas allgemeinere Funktion zu übergeben, auf die kein Zugriff erforderlich ist Jedes private Mitglied von PredicateList, und dies wird oft auch dazu neigen, dieser internen Funktion einen klareren, allgemeineren Namen und Zweck zu geben sowie mehr Möglichkeiten für die "Wiederverwendung von Code im Nachhinein".
Dragon Energy