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
Foo
als Mitglied deklarieren , müssen Sie diese einschließen foo.h
. Dies bedeutet, dass das Ändern der Implementierung von Foo::f
Propagates 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::f
drinnen bar.cpp
. Wenn Sie die Umsetzung jedoch ändern Foo::f
, beide bar.cpp
und qux.cpp
Bedarf neu kompiliert werden. Die Implementierung von Foo::f
Leben in beiden Dateien, obwohl kein Teil von Qux
direkt 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
Foo
einer Klasse ein Mitglied hinzufügen Bar
, müssen Sie es foo.h
in die Kopfzeile aufnehmen. Wenn Sie Foo::f
eine .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::f
werden, 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.tpp
in 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.cpp
auf die Neuerstellung : Muss nur neu kompiliert werden, wenn Sie die Implementierung von ändern Foo::f
. Die Datei qux.cpp
muss 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.