C ++ Bevorzugte Methode für die Implementierung großer Vorlagen

10

Wenn Sie eine C ++ - Klasse deklarieren, empfiehlt es sich normalerweise, nur die Deklaration in die Header-Datei und die Implementierung in eine Quelldatei einzufügen. Es scheint jedoch, dass dieses Entwurfsmodell für Vorlagenklassen nicht funktioniert.

Wenn Sie online suchen, scheint es zwei Meinungen darüber zu geben, wie Vorlagenklassen am besten verwaltet werden können:

1. Gesamte Deklaration und Implementierung im Header.

Dies ist ziemlich einfach, führt aber meiner Meinung nach dazu, dass es schwierig ist, Codedateien zu pflegen und zu bearbeiten, wenn die Vorlage groß wird.

2. Schreiben Sie die Implementierung in eine am Ende enthaltene Template-Include-Datei (.tpp).

Dies scheint mir eine bessere Lösung zu sein, scheint aber nicht weit verbreitet zu sein. Gibt es einen Grund, warum dieser Ansatz minderwertig ist?

Ich weiß, dass der Codestil oft von persönlichen Vorlieben oder Legacy-Stilen bestimmt wird. Ich starte ein neues Projekt (Portierung eines alten C-Projekts nach C ++) und bin relativ neu im OO-Design und möchte von Anfang an Best Practices befolgen.

Fhorrobin
quelle
1
Siehe diesen 9 Jahre alten Artikel auf codeproject.com. Methode 3 haben Sie beschrieben. Scheint nicht so besonders zu sein, wie Sie glauben.
Doc Brown
.. oder hier, gleicher Ansatz, Artikel aus dem Jahr 2014: codeofhonour.blogspot.com/2014/11/…
Doc Brown
2
Eng verwandt: stackoverflow.com/q/1208028/179910 . Gnu verwendet normalerweise eine ".tcc" -Erweiterung anstelle von ".tpp", ist aber ansonsten ziemlich identisch.
Jerry Coffin
Ich habe immer "ipp" als Erweiterung verwendet, aber ich habe das Gleiche in dem Code getan, den ich geschrieben habe.
Sebastian Redl

Antworten:

6

Wenn Sie eine C ++ - Klasse mit Vorlagen schreiben, haben Sie normalerweise drei Möglichkeiten:

(1) Deklaration und Definition in die Kopfzeile einfügen.

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f()
    {
        ...
    }
};

oder

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f();
};

template <typename T>
inline void Foo::f()
{
    ...
}

Profi:

  • Sehr bequeme Verwendung (nur den Header einschließen).

Con:

  • Schnittstellen- und Methodenimplementierung sind gemischt. Dies ist "nur" ein Lesbarkeitsproblem. Einige finden dies nicht wartbar, weil es sich vom üblichen .h / .cpp-Ansatz unterscheidet. Beachten Sie jedoch, dass dies in anderen Sprachen, z. B. C # und Java, kein Problem darstellt.
  • Hohe Auswirkungen auf die Wiederherstellung: Wenn Sie eine neue Klasse Fooals Mitglied deklarieren , müssen Sie diese einschließen foo.h. Dies bedeutet, dass das Ändern der Implementierung von Foo::fPropagates sowohl über Header- als auch über Quelldateien erfolgt.

Schauen wir uns die Auswirkungen der Neuerstellung genauer an: Bei C ++ - Klassen ohne Vorlage fügen Sie Deklarationen in .h und Methodendefinitionen in .cpp ein. Auf diese Weise muss beim Ändern der Implementierung einer Methode nur eine CPP neu kompiliert werden. Dies ist für Vorlagenklassen anders, wenn die .h Ihren gesamten Code enthält. Schauen Sie sich das folgende Beispiel an:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Hier ist die einzige Verwendung von Foo::fdrinnen bar.cpp. Wenn Sie die Umsetzung jedoch ändern Foo::f, beide bar.cppund qux.cppBedarf neu kompiliert werden. Die Implementierung von Foo::fLeben in beiden Dateien, obwohl kein Teil von Quxdirekt etwas von verwendet Foo::f. Bei großen Projekten kann dies bald zum Problem werden.

(2) Fügen Sie die Deklaration in .h und die Definition in .tpp ein und fügen Sie sie in .h ein.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};
#include "foo.tpp"    

// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
    ...
}

Profi:

  • Sehr bequeme Verwendung (nur den Header einschließen).
  • Schnittstellen- und Methodendefinitionen sind getrennt.

Con:

  • Hohe Auswirkungen auf den Wiederaufbau (wie bei (1) ).

Diese Lösung trennt Deklaration und Methodendefinition in zwei separaten Dateien, genau wie .h / .cpp. Dieser Ansatz hat jedoch das gleiche Wiederherstellungsproblem wie (1) , da der Header direkt die Methodendefinitionen enthält.

(3) Fügen Sie die Deklaration in .h und die Definition in .tpp ein, schließen Sie jedoch .tpp nicht in .h ein.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};

// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
    ...
}

Profi:

  • Reduziert die Auswirkungen auf die Wiederherstellung genauso wie die Trennung von .h / .cpp.
  • Schnittstellen- und Methodendefinitionen sind getrennt.

Con:

  • Unbequeme Verwendung: Wenn Sie Fooeiner Klasse ein Mitglied hinzufügen Bar, müssen Sie es foo.hin die Kopfzeile aufnehmen. Wenn Sie Foo::feine .cpp aufrufen, müssen Sie diese auch einschließen foo.tpp.

Dieser Ansatz reduziert die Auswirkungen auf die Neuerstellung, da nur CPP-Dateien, die wirklich verwendet Foo::fwerden, neu kompiliert werden müssen. Dies hat jedoch einen Preis: Alle diese Dateien müssen enthalten sein foo.tpp. Nehmen Sie das Beispiel von oben und verwenden Sie den neuen Ansatz:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Wie Sie sehen können, ist der einzige Unterschied das zusätzliche Einschließen von foo.tppin bar.cpp. Dies ist unpraktisch und das Hinzufügen eines zweiten Includes für eine Klasse, je nachdem, ob Sie Methoden aufrufen, erscheint sehr hässlich. Sie reduzieren jedoch die Auswirkungen bar.cppauf die Neuerstellung : Muss nur neu kompiliert werden, wenn Sie die Implementierung von ändern Foo::f. Die Datei qux.cppmuss nicht neu kompiliert werden.

Zusammenfassung:

Wenn Sie eine Bibliothek implementieren, müssen Sie sich normalerweise nicht um die Auswirkungen der Wiederherstellung kümmern. Benutzer Ihrer Bibliothek greifen auf eine Version zu und verwenden sie. Die Implementierung der Bibliothek ändert sich in der täglichen Arbeit des Benutzers nicht. In solchen Fällen kann die Bibliothek den Ansatz (1) oder (2) verwenden, und es ist nur eine Frage des Geschmacks, welchen Sie wählen.

Wenn Sie jedoch an einer Anwendung oder an einer internen Bibliothek Ihres Unternehmens arbeiten, ändert sich der Code häufig. Sie müssen sich also um die Wiederherstellung der Auswirkungen kümmern. Die Wahl von Ansatz (3) kann eine gute Option sein, wenn Sie Ihre Entwickler dazu bringen, das zusätzliche Include zu akzeptieren.

pschill
quelle
2

Ähnlich wie bei der .tppIdee (die ich noch nie gesehen habe) haben wir die meisten Inline-Funktionen in eine -inl.hppDatei eingefügt, die am Ende der üblichen .hppDatei enthalten ist.

Wie andere angeben, bleibt die Benutzeroberfläche dadurch lesbar, indem die Unordnung der Inline-Implementierungen (wie Vorlagen) in eine andere Datei verschoben wird. Wir erlauben einige Schnittstellen-Inlines, versuchen jedoch, sie auf kleine, normalerweise einzeilige Funktionen zu beschränken.

Bill Door
quelle
1

Eine Pro-Münze der 2. Variante ist, dass Ihre Header aufgeräumter aussehen.

Der Nachteil könnte sein, dass Sie die Inline-IDE-Fehlerprüfung und die Debugger-Bindungen vermasselt haben.

πάντα ῥεῖ
quelle
2. erfordert auch viel Redundanz bei der Deklaration von Vorlagenparametern, was insbesondere bei Verwendung von sfinae sehr ausführlich werden kann. Und im Gegensatz zum OP finde ich das 2. schwieriger zu lesen, je mehr Code vorhanden ist, insbesondere wegen der redundanten Boilerplate.
Sopel
0

Ich bevorzuge den Ansatz, die Implementierung in eine separate Datei zu stellen und nur die Dokumentation und Deklarationen in der Header-Datei zu haben.

Vielleicht haben Sie diesen Ansatz in der Praxis nicht oft gesehen, weil Sie nicht an den richtigen Stellen gesucht haben ;-)

Oder - vielleicht liegt es daran, dass die Entwicklung der Software etwas mehr Aufwand erfordert. Aber für eine Klassenbibliothek lohnt sich diese Anstrengung, IMHO, und macht sich in einer viel einfacher zu verwendenden / lesbaren Bibliothek bezahlt.

Nehmen Sie zum Beispiel diese Bibliothek: https://github.com/SophistSolutions/Stroika/

Die gesamte Bibliothek wird mit diesem Ansatz geschrieben. Wenn Sie den Code durchsehen, werden Sie sehen, wie gut er funktioniert.

Die Header-Dateien sind ungefähr so ​​lang wie die Implementierungsdateien, aber sie sind nur mit Deklarationen und Dokumentationen gefüllt.

Vergleichen Sie die Lesbarkeit von Stroika mit der Ihrer bevorzugten Standard-C ++ - Implementierung (gcc oder libc ++ oder msvc). Diese verwenden alle den Inline-In-Header-Implementierungsansatz, und obwohl sie sehr gut geschrieben sind, sind sie meiner Meinung nach nicht als lesbare Implementierungen.

Lewis Pringle
quelle