Sind Utility-Klassen mit nichts als statischen Elementen in C ++ ein Anti-Pattern?

39

Die Frage, wo ich Funktionen platzieren soll, die sich nicht auf eine Klasse beziehen, hat einige Debatten ausgelöst, ob es in C ++ sinnvoll ist, Dienstprogrammfunktionen in einer Klasse zu kombinieren, oder ob sie nur als freie Funktionen in einem Namespace vorhanden sind.

Ich komme aus einem C # -Hintergrund, in dem die letztere Option nicht existiert, und tendiere daher natürlich dazu, statische Klassen in dem kleinen C ++ - Code zu verwenden, den ich schreibe. Die am höchsten bewertete Antwort auf diese Frage sowie mehrere Kommentare besagen jedoch, dass freie Funktionen vorzuziehen sind, selbst wenn man annimmt, dass statische Klassen ein Anti-Muster sind. Warum ist das so in C ++? Zumindest an der Oberfläche scheinen statische Methoden für eine Klasse von freien Funktionen in einem Namespace nicht zu unterscheiden zu sein. Warum also die Präferenz für Letzteres?

Wäre es anders, wenn die Sammlung von Utility-Funktionen einige gemeinsam genutzte Daten erfordern würde, z. B. einen Cache, den man in einem privaten statischen Feld speichern könnte?

PersonalNexus
quelle
Klingt ein bisschen wie das Antimuster "funktionale Zersetzung".
user281377
7
Kurze Antwort: Sie brauchen nur keine Klasse, um diese Funktionen zu verpacken. Freie Funktionen passen viel besser zu Ihrer Aufgabe als sie in ein Pseudo-OO-Konstrukt zu zerlegen. Dies ist eine Umgehung, die Sie nur in "rein-OO" -Sprachen benötigen.
Chris sagt Reinstate Monica

Antworten:

39

Ich denke zu antworten, dass wir die Absichten von Klassen und Namespaces vergleichen sollten. Laut Wikipedia:

Klasse

In der objektorientierten Programmierung ist eine Klasse ein Konstrukt, das als Entwurf verwendet wird, um Instanzen von sich selbst zu erstellen - bezeichnet als Klasseninstanzen, Klassenobjekte, Instanzobjekte oder einfach Objekte. Eine Klasse definiert konstituierende Mitglieder, die es diesen Klasseninstanzen ermöglichen, Status und Verhalten zu haben. Datenfeldelemente (Elementvariablen oder Instanzvariablen) ermöglichen es einem Klassenobjekt, den Status beizubehalten. Andere Arten von Elementen, insbesondere Methoden, aktivieren das Verhalten eines Klassenobjekts. Klasseninstanzen sind vom Typ der zugeordneten Klasse.

Namespace

Im Allgemeinen ist ein Namespace ein Container, der den Kontext für die darin enthaltenen Bezeichner (Namen oder Fachbegriffe oder Wörter) bereitstellt und die Disambiguierung von Homonym-Bezeichnern ermöglicht, die sich in verschiedenen Namespaces befinden.

Was möchten Sie nun erreichen, indem Sie die Funktionen in eine Klasse (statisch) oder einen Namespace einfügen? Ich würde wetten, dass die Definition eines Namensraums Ihre Absicht besser beschreibt - alles, was Sie wollen, ist ein Container für Ihre Funktionen. Sie benötigen keine der in der Klassendefinition beschriebenen Funktionen. Beachten Sie, dass die ersten Wörter der Klassendefinition " In der objektorientierten Programmierung " lauten und dennoch nichts an einer Sammlung von Funktionen objektorientiert ist.

Es gibt wahrscheinlich auch technische Gründe, aber da jemand aus Java kommt und versucht, mich mit der Multiparadigmasprache C ++ vertraut zu machen, ist die naheliegendste Antwort für mich: Weil wir kein OO benötigen, um dies zu erreichen.

Gyan alias Gary Buyn
quelle
1
+1 für "Wir brauchen keine OO, um dies zu erreichen"
Ixrec
Ich bin damit einverstanden, dass kein Fan von OO ist, aber ich denke, dass es einige Situationen gibt, in denen die Verwendung einer All-static-Klasse die einzige Lösung ist, zum Beispiel das Ersetzen eines Namespaces, da Sie keinen verschachtelten Namespace darin deklarieren können eine Klasse.
Wael Boutglay
26

Ich würde das sehr vorsichtig als Anti-Pattern bezeichnen. Namespaces werden normalerweise bevorzugt, aber da es keine Namespace-Vorlagen gibt und Namespaces nicht als Vorlagenparameter übergeben werden können, ist die Verwendung von Klassen mit ausschließlich statischen Elementen durchaus üblich.

Ein Programmierer
quelle
3
Die anderen Antworten haben die letzte Zeile des OP beschönigt. Namespaces sind so gut wie benannte Bereiche. Wenn Sie also eine Zugriffssteuerung benötigen, sind Klassen das einzige verfügbare Tool.
cmannett85
2
@cbamber85 um fair zu sein, ich hatte diese Zeile erst eingefügt, nachdem ich die ersten beiden Antworten gelesen hatte.
PersonalNexus
@PersonalNexus Ah fair genug, ich wusste es nicht!
cmannett85
10
@ cbamber85: Das stimmt nicht ganz. Sie erhalten eine noch bessere Kapselung, wenn Sie "private" Funktionen in die Implementierungsdatei einfügen, sodass die Benutzer nicht einmal die private Deklaration sehen können.
UncleBens
2
+ 1-Vorlagen sind in der Tat ein Punkt, an dem Namespaces noch keine Funktionen haben.
Chris sagt Reinstate Monica
13

Früher musste ich einen FORTRAN-Kurs belegen. Zu diesem Zeitpunkt kannte ich andere Gebotssprachen und dachte mir, ich könnte FORTRAN ohne viel Lernen machen. Als ich meine ersten Hausaufgaben abgab, schickte der Professor sie mir zurück und bat, sie zu wiederholen: Er sagte, dass Pascal-Programme, die in FORTRAN-Syntax geschrieben sind, nicht als gültige Einsendungen gelten.

Hier spielt ein ähnliches Problem eine Rolle: Die Verwendung statischer Klassen zum Hosten von Dienstprogrammfunktionen in C ++ ist eine Fremdsprache für C ++.

Wie Sie in Ihrer Frage erwähnt haben, ist die Verwendung statischer Klassen für Dienstprogrammfunktionen in C # eine Notwendigkeit: Freistehende Funktionen sind in C # einfach keine Option. Die Sprache, die benötigt wird, um ein Muster zu entwickeln, mit dem Programmierer freistehende Funktionen auf andere Weise definieren können - nämlich innerhalb statischer Dienstprogrammklassen. Dies war ein Wort-für-Wort-Java-Trick: Zum Beispiel sind java.lang.Math und System.Math von .NET nahezu isomorph.

C ++ bietet jedoch Namespaces, eine andere Möglichkeit, um dasselbe Ziel zu erreichen, und verwendet sie aktiv bei der Implementierung seiner Standardbibliothek. Das Hinzufügen einer zusätzlichen Schicht statischer Klassen ist nicht nur unnötig, sondern auch für Leser ohne C # - oder Java-Hintergrund etwas weniger intuitiv. In gewisser Weise führen Sie eine "Kreditübersetzung" in die Sprache ein, die von Haus aus ausgedrückt werden kann.

Wenn Ihre Funktionen Daten gemeinsam nutzen müssen, ist die Situation anders. Da Ihre Funktionen nicht mehr voneinander unabhängig sind , wird das Singleton-Muster zur bevorzugten Methode, um diese Anforderung zu erfüllen.

dasblinkenlight
quelle
6
+1, außer dass Singletons empfohlen werden. Sie werden in C ++ nicht bevorzugt! Funktionen können in einer Klasse sein, wenn sie den Status gemeinsam nutzen müssen, Klassen sollten jedoch keine Singletons sein, es sei denn, sie müssen es wirklich wirklich sein. Und das tun sie normalerweise nicht.
Maxpm 11.02.12
@Maxpm Verstehen Sie mich nicht falsch, Singleton wird nur globalen statischen Variablen vorgezogen. Davon abgesehen gibt es nichts "Bevorzugtes".
dasblinkenlight
2
Es gibt diesen guten Artikel, Singletons: Lösen von Problemen, von denen Sie nicht wussten, dass Sie sie seit 1995 noch nie hatten . Zusammenfassend heißt es, dass das Koppeln der bekannten Instanz an die Klasse selbst Ihre Flexibilität unnötig einschränkt und Ihnen nichts gibt. Die meiste Zeit haben Sie nur eine Klasse und haben irgendwo eine gemeinsame Instanz, aber machen Sie es nicht singleton.
Jan Hudec
Ich bin mir nicht sicher, ob "Fremdsprache" der richtige Begriff ist. Es ist eine sehr verbreitete Redewendung. Ich denke, einige Programmierteams verwenden e2 von Stroustrup ...
Nick Keighley
10

Vielleicht müssen Sie sich fragen, warum Sie eine rein statische Klasse wollen?

Der einzige Grund, den ich mir vorstellen kann, ist, dass andere Sprachen (Java und C #), bei denen alles eine Klasse ist, dies erfordern. Diese Sprachen können überhaupt keine Funktionen der obersten Ebene erstellen, daher haben sie einen Trick erfunden, um sie beizubehalten, und das war die statische Elementfunktion. Sie sind eine Art Workaround, aber C ++ benötigt so etwas nicht. Sie können direkt brandneue, unabhängige Funktionen auf höchster Ebene erstellen.

Wenn Sie Funktionen benötigen, die für ein bestimmtes Datenelement ausgeführt werden, ist es sinnvoll, sie in einer Klasse zu bündeln, die die Daten enthält. In diesem Fall sind diese Funktionen jedoch keine weiteren Mitglieder dieser Klasse, die für die Daten der Klasse ausgeführt werden.

Wenn Sie eine Funktion haben, die nicht mit einem bestimmten Datentyp arbeitet (ich verwende das Wort hier, da Klassen Wege sind, um neue Datentypen zu definieren), dann ist es wirklich ein Anti-Pattern, sie in eine Klasse zu schieben, für keinen anderen als semantische Zwecke.

gbjbaanb
quelle
10
In der Tat - ich würde sagen, dass die "Klasse mit allen statischen Mitgliedern" in Java eigentlich ein Hack ist , um eine Einschränkung von Java zu umgehen.
Charles Salvia
@CharlesSalvia: Ich war höflich :) Ich habe das gleiche schlechte Gefühl, dass main () auch Teil einer Java-Klasse ist, obwohl ich verstehe, warum sie das getan haben.
gbjbaanb
Dies scheint tatsächlich die Antwort auf die Frage zu sein, warum Sie eine vollständig statische Klasse wünschen. Oder verstehe ich dich falsch? Zum Beispiel muss ich eine Variable enthalten, deren Wert sich ändern kann, die von einer Klasse (die ich im ROM speichern möchte) gelesen und von einer anderen Klasse (die sich im RAM befindet) geschrieben wird. Es reicht nicht aus, die an einem bekannten Speicherort zu platzieren. Wenn ich sie (und ihre Zugriffsmechanismen) in eine Klasse einbinde, kann ich die Zugriffskontrolle optimieren. So ziemlich das, was Sie in Ihrem zweiten Absatz beschreiben.
Iheanyi
5

Zumindest an der Oberfläche scheinen statische Methoden für eine Klasse von freien Funktionen in einem Namespace nicht zu unterscheiden zu sein.

Mit anderen Worten, eine Klasse anstelle eines Namespaces zu haben, hat keine Vorteile.

Warum also die Präferenz für Letzteres?

Zum einen erspart es Ihnen, die staticganze Zeit zu tippen, obwohl das wohl ein eher geringfügiger Vorteil ist.

Der Hauptvorteil ist, dass es das am wenigsten leistungsfähige Werkzeug für den Job ist. Klassen können zum Erstellen von Objekten verwendet werden. Sie können als Variablentyp oder als Vorlagenargumente verwendet werden. Keine dieser Funktionen möchten Sie für Ihre Funktionssammlung verwenden. Verwenden Sie daher vorzugsweise ein Tool ohne diese Funktionen, damit die Klasse nicht versehentlich missbraucht wird.

Wenn Sie einem Namespace folgen, wird den Benutzern Ihres Codes auch sofort klar, dass es sich um eine Sammlung von Funktionen und nicht um eine Blaupause handelt, aus der Objekte erstellt werden können.

sepp2k
quelle
5

... In mehreren Kommentaren heißt es jedoch, dass freie Funktionen vorzuziehen sind, und sogar, dass statische Klassen ein Anti-Muster waren. Warum ist das so in C ++? Zumindest an der Oberfläche scheinen statische Methoden für eine Klasse von freien Funktionen in einem Namespace nicht zu unterscheiden zu sein. Warum also die Präferenz für Letzteres?

Eine rein statische Klasse wird die Arbeit erledigen, aber es ist, als würde man mit einem 53-Fuß-Sattelschlepper zum Supermarkt fahren, um Chips und Salsa zu kaufen, wenn eine viertürige Limousine dies tut (dh es ist übertrieben). Klassen sind mit einem geringen zusätzlichen Aufwand verbunden, und ihre Existenz könnte den Eindruck erwecken, dass es eine gute Idee sein könnte, eine Instanz zu erstellen. Die freien Funktionen von C ++ (und C, wo alle Funktionen frei sind) machen das nicht; Sie sind nur Funktionen und sonst nichts.

Wäre es anders, wenn die Sammlung von Utility-Funktionen einige gemeinsam genutzte Daten erfordern würde, z. B. einen Cache, den man in einem privaten statischen Feld speichern könnte?

Nicht wirklich. Der ursprüngliche Zweck von staticC (und höherem C ++) bestand darin, dauerhaften Speicher anzubieten, der auf jeder Ebene des Umfangs verwendet werden kann:

int counter() {
    static int value = 0;
    value++;
}

Sie können den Bereich auf die Ebene einer Datei verschieben, wodurch er für alle engeren Bereiche sichtbar ist, jedoch nicht außerhalb der Datei:

static int value = 0;

int up() { value++; }
int down() { value--; }

Ein privates statisches Klassenmitglied in C ++ dient demselben Zweck, ist jedoch auf den Bereich einer Klasse beschränkt. Die im counter()obigen Beispiel verwendete Technik funktioniert auch innerhalb von C ++ - Methoden. Ich würde diese Vorgehensweise empfehlen, wenn die Variable nicht für die gesamte Klasse sichtbar sein muss.

Blrfl
quelle
Ich nehme an, dass eine Gruppe nicht verwandter Dienstprogrammfunktionen, die keine Daten gemeinsam haben, besser in einem Namespace gruppiert werden sollte. Wenn Sie jedoch über eine Gruppe eng verzahnter Dienstprogrammfunktionen verfügen, die Zugriff auf gemeinsam genutzte Daten und möglicherweise eine Zugriffssteuerung benötigen, ist eine statische Klasse die beste Wahl. Ein Statik innerhalb einer Funktion ist nicht wiedereintrittsfähig. Verwenden eines Moduls global ... in diesem Zusammenhang etwas besser, aber ich denke immer noch, dass eine vollständig statische Klasse die beste Lösung ist, wenn Sie die von mir beschriebenen Anforderungen haben.
Iheanyi
Wenn sie Daten gemeinsam nutzen, sind sie möglicherweise besser als eine Klasse ohne statische Methoden?
Nick Keighley
@NickKeighley Das hängt davon ab, wer die Debatte gewinnt, ob es besser ist, eine Instanz zu erstellen und sie an alles weiterzugeben, was sie benötigt, oder sie einfach in der Klasse statisch zu machen.
Blrfl
1

Wenn eine Funktion keinen Zustand beibehält und wieder eintritt, scheint es nicht sinnvoll zu sein, sie innerhalb einer Klasse zu verschieben (es sei denn, dies wird von der Sprache erzwungen). Wenn die Funktion einen bestimmten Status beibehält (z. B. nur mit einem statischen Mutex threadsicher gemacht werden kann), erscheinen statische Methoden für eine Klasse angemessen.

Martin James
quelle
1

Ein Großteil des Diskurses zu diesem Thema ist sinnvoll, obwohl C ++ etwas sehr Grundlegendes hat, das namespacesund classes / structs sehr unterschiedlich macht.

Statische Klassen (Klassen, in denen alle Mitglieder statisch sind und die Klasse niemals instanziiert wird) sind selbst Objekte. Sie sind nicht einfach namespaceFunktionen zu enthalten.

Die Template-Metaprogrammierung ermöglicht die Verwendung einer statischen Klasse als Objekt zur Kompilierungszeit.

Bedenken Sie:

template<typename allocator_type> class allocator
{
public:
    inline static void* allocate(size_t size)
    {
        return allocator_type::template allocate(size);
    }
    inline static void release(void* p)
    {
        allocator_type::template release(p);
    }
};

Um es zu benutzen, brauchen wir Funktionen, die in einer Klasse enthalten sind. Ein Namespace funktioniert hier nicht. Erwägen:

class mallocator
{
    inline static void* allocate(size_t size)
    {
        return std::malloc(size);
    }
    inline static void release(void* p)
    {
        return std::free(p);
    }
};

Um es jetzt zu benutzen:

using my_allocator = allocator<mallocator>;

void* p = my_allocator::allocate(1024);
...
my_allocator::release(p);

Solange ein neuer Allokator eine allocateund releasekompatible Funktion verfügbar macht , ist der Wechsel zu einem neuen Allokator einfach.

Dies kann mit Namespaces nicht erreicht werden.

Benötigen Sie immer Funktionen, um Teil einer Klasse zu sein? Nein.

Ist die Verwendung einer statischen Klasse ein Anti-Pattern? Es kommt auf den Kontext an.

Wäre es anders, wenn die Sammlung von Utility-Funktionen einige gemeinsam genutzte Daten erfordern würde, z. B. einen Cache, den man in einem privaten statischen Feld speichern könnte?

In diesem Fall wird das, was Sie erreichen möchten, wahrscheinlich am besten durch objektorientierte Programmierung erreicht.

Michael Gazonda
quelle
Die Verwendung des Schlüsselworts inline ist hier überflüssig, da in einer Klassendefinition definierte Methoden implizit inline sind. Siehe de.cppreference.com/w/cpp/language/inline .
Trevor
1

Wie John Carmack berühmt sagte:

"Manchmal ist die elegante Implementierung nur eine Funktion. Keine Methode. Keine Klasse. Kein Framework. Nur eine Funktion."

Ich denke, das fasst es ziemlich gut zusammen. Warum würdest du es mit Nachdruck zu einer Klasse machen, wenn es eindeutig keine Klasse ist? C ++ hat den Luxus, dass Sie tatsächlich Funktionen verwenden können; In Java ist alles eine Methode. Dann brauchen Sie Utility-Klassen und viele von ihnen.

juhist
quelle