Klassen-Templates

Modernes C++ Rainer Grimm  –  24 Kommentare

Ein Funktions-Template repräsentiert eine Familie von Funktionen. Entsprechend repräsentiert ein Klassen-Template eine Familie von Klassen. Heute möchte ich Klassen-Templates vorstellen.

Klassen-Templates

Ein Klassen-Template zu definieren ist einfach.

Definition eines Klassen-Templates

Angenommen, du hast eine Klasse Array, die ein Klassen-Template werden soll.

class Array{

public:
int getSize() const {
return 10;
}

private:
int elem[10];
};

Die Klasse Array enthält ein C-Array vom Typ int mit der Länge 10. Der Typ des C-Arrays und seine Länge sind offensichtliche Erweiterungspunkte. Lass mich ein Klassen-Template erstellen, indem wir einen Typ-Parameter T und einen Nicht-Typ-Parameter N einführen und damit spielen.

// arrayClassTemplate.cpp

#include <cstddef> // (1)
#include <iostream>
#include <string>

template <typename T, std::size_t N> // (2)
class Array{

public:
std::size_t getSize() const {
return N;
}

private:
T elem[N]
};

int main() {

std::cout << '\n';

Array<int, 100> intArr; // (3)
std::cout << "intArr.getSize(): " << intArr.getSize() << '\n';

Array<std::string, 5> strArr; // (4)
std::cout << "strArr.getSize(): " << strArr.getSize() << '\n';

Array<Array<int, 3>, 25> intArrArr; // (5)
std::cout << "intArrArr.getSize(): " << intArrArr.getSize() << '\n';

std::cout << '\n';

}

Das Array wird durch seinen Typ und seine Größe parametrisiert. Für die Größe habe ich den vorzeichenlosen Integer-Typ std::size_t (2) verwendet, der die maximale Größe speichern kann. Um std::size_t zu verwenden, muss ich den Header <cstddef> (1) einbinden. Jetzt kann das Array mit einem int (3), einem std::string (4) und einem Array<int, 3> (5) instanziiert werden. Der folgende Screenshot zeigt die Ausgabe des Programms.

Klassen-Templates

Du kannst die Memberfunktionen eines Templates innerhalb und außerhalb des Klassen-Templates definieren.

Definitionen der Memberfunktionen

Die Definition der Member-Funktionen innerhalb des Klassen-Templates ist ganz intuitiv.

template <typename T, std::size_t N>   
class Array{

public:
std::size_t getSize() const {
return N;
}

private:
T elem[N]
};

Wenn du die Memberfunktionen außerhalb der Klasse definierst, musst Du angeben, dass es sich um ein Template handelt. Dazu ist es notwendig, die volle Typqualifikation des Klassen-Templates anzugeben. Das modifizierte Klassen-Template Array sieht dann so aus:

template <typname T, std::size_t N> 
class Array{

public:
std::size_t getSize() const;

private:
T elem[N]
};

template <typename T, std::size_t N> // (1)
std::size_t Array<T, N>::getSize() const {
return N;
}

(1) ist die Memberfunktion getSize des Arrays, die außerhalb der Klasse definiert ist. Die Memberfunktion außerhalb des Klassen-Templates zu definieren, wird aufwändig, wenn die Memberfunktion selbst ein Template ist.

Memberfunktionen als Templates

Ein typisches Beispiel für eine generische Memberfunktion ist ein generischer Zuweisungsoperator. Der Grund für einen generischen Zuweisungsoperator ist naheliegend. Ein Array<T, N> soll sich einem Array<T2, N2> zuweisen lassen, wenn der Datentyp T sich T2 zuweisen lässt und beide Arrays die gleiche Größe besitzen.

Das Zuweisen eines Array<float, 5> an ein Array<double, 5> ist nicht gültig, da beide Arrays unterschiedliche Typen besitzen.

// arrayAssignmentError.cpp

#include <cstddef>
#include <iostream>
#include <string>

template <typename T, std::size_t N>
class Array{

public:
std::size_t getSize() const {
return N;
}

private:
T elem[N]
};

int main() {

std::cout << '\n';

Array<float, 5> floatArr;
Array<float, 5> floatArr2;

floatArr2 = floatArr; // (1)


Array<double, 5> doubleArr;
doubleArr = floatArr; // (2)


}

Das Zuweisen von floatArr an floatArr2 (1) ist gültig, da beide Arrays den gleichen Typ haben. Das Zuweisen von floatArr an doubleArr ist hingegen nicht gültig (2). Der Compiler beschwert sich, dass es keine Konvertierung von Array<float, 5> zu einem Array<double, 5> gibt.

Klassen-Templates

Hier ist eine naive Implementierung der Klasse Array, die die Zuweisung von zwei Arrays gleicher Länge unterstützt. Das C-Array elem ist absichtlich public.

template <typename T, std::size_t N>   
class Array{

public:
template <typename T2>
Array<T, N>& operator = (const Array<T2, N>& arr) {
std::copy(std::begin(arr.elem), std::end(arr.elem), std::begin(elem));
return *this;
}
std::size_t getSize() const {
return N;
}
T elem[N];

};

Der Zuweisungsoperator Array<T, N>& operator = (const Array<T2, N>& arr) akzeptiert Arrays, die im zugrundeliegenden Typ variieren können, aber nicht in der Länge. Bevor ich den vollständigen Code vorstelle, möchte ich ihn noch sukzessive verbessern.

Freundschaft

Wenn elem als private definiert wird, muss Array zum Freund der Klasse werden.

template <typename T, std::size_t N>   
class Array{

public:
template <typename T2>
Array<T, N>& operator = (const Array<T2, N>& arr) {
std::copy(std::begin(arr.elem), std::end(arr.elem), std::begin(elem));
return *this;
}
template<typename, std::size_t> friend class Array; // (1)
std::size_t getSize() const {
return N;
}
private:
T elem[N]

};

Die Zeile template<typename, std::size_t> friend class Array (1) erklärt alle Instanzen von Array zu Freunden.

Memberfunktionen außerhalb der Klasse definiert

Die generische Memberfunktion außerhalb der Klasse zu definieren ist ein wenig mühsam.

template <typename T, std::size_t N>   
class Array{

public:
template <typename T2>
Array<T, N>& operator = (const Array<T2, N>& arr);
template<typename, std::size_t> friend class Array;
std::size_t getSize() const;
private:
T elem[N]

};

template <typename T, std::size_t N>
std::size_t Array<T, N>::getSize() const { return N; }

template<typename T, std::size_t N> // (1)
template<typename T2>
Array<T, N>& Array<T, N>::operator = (const Array<T2, N>& arr) {
std::copy(std::begin(arr.elem), std::end(arr.elem), std::begin(elem));
return *this;
}

Es ist für eine außerhalb des Klassenkörpers definierte generische Memberfunktion (1) notwendig, dass die Klasse und die Memberfunktionen Templates sind. Zusätzlich muss die vollständige Typqualifikation der generischen Memberfunktion angeben werden. In der Klasse Array wird der Zuweisungsoperator auch für Datentypen T und T2 eingesetzt, die nicht konvertierbar sind. Das Aufrufen des Zuweisungsoperators mit nichtkonvertierbaren Typen führt natürlich zu einer "hässlichen" Fehlermeldung. Dieses Problem sollte ich beheben.

Anforderungen an die Typparameter

Die Anforderungen lassen sich mit der type traits library und static_assert (C++11) oder mit Concepts (C++20) formulieren. Hier sind die beiden Varianten des generischen Zuweisungsoperators:

  • C++11
template<typename T, std::size_t N>
template<typename T2>
Array<T, N>& Array<T, N>::operator = (const Array<T2, N>& arr) {
static_assert(std::is_convertible<T2, T>::value, // (1)
"Cannot convert the source type into the destination type!");
std::copy(std::begin(arr.elem), std::end(arr.elem), std::begin(elem));
return *this;
}
  • C++20

Abschließend ist hier das komplette Programm unter Verwendung des Concepts std::convertible_to in der Deklaration (1) und der Definition (2) der Memberfunktion.

// arrayAssignment.cpp

#include <algorithm>
#include <cstddef>
#include <iostream>
#include <string>
#include <concepts>

template <typename T, std::size_t N>
class Array{

public:
template <typename T2>
Array<T, N>& operator = (const Array<T2, N>& arr) requires std::convertible_to<T2, T>; // (1)
template<typename, std::size_t> friend class Array;
std::size_t getSize() const;
private:
T elem[N];

};

template <typename T, std::size_t N>
std::size_t Array<T, N>::getSize() const { return N; }

template<typename T, std::size_t N>
template<typename T2>
Array<T, N>& Array<T, N>::operator = (const Array<T2, N>& arr) requires std::convertible_to<T2, T> { // (2)
std::copy(std::begin(arr.elem), std::end(arr.elem), std::begin(elem));
return *this;
}

int main() {

std::cout << '\n';

Array<float, 5> floatArr;
Array<float, 5> floatArr2;
floatArr.getSize();

floatArr2 = floatArr;


Array<double, 5> doubleArr;
doubleArr = floatArr;

Array<std::string, 5> strArr;
// doubleArr = strArr; // (3)

}

Wenn ich den Ausdruck (3) verwende, beschwert sich der GCC im Wesentlichen darüber, dass die Einschränkungen nicht erfüllt werden.

Wie geht es weiter?

Natürlich bin ich noch nicht fertig mit meinen Artikeln zu Klassen-Templates. In meinem nächsten Artikel schreibe ich über zwei knifflige Details: Vererbung von Klassen-Templates und die Instanziierung von Memberfunktionen von Klassen-Templates

The Next PDF-Bundle

I want to resuscitate an old service and create bundles about old posts. I will create the bundles only for my English posts because this is quite a job. These bundles include the posts, all source files, and a cmake file. In order for me to make the right decision, you have to make your cross. I will build the pdf bundle with the most votes. The vote is open until 30th of May (including). Vote here.