C++ Core Guidelines: Template-Definitionen

Modernes C++  –  2 Kommentare

Template-Definitionen beschäftigen sich mit Regeln, die typisch für die Implementierung eines Templates sind. Das bedeutet insbesondere auch, wie stark die Template-Definition von ihrem Kontext abhängt.

Hier sind die Regeln, um die es im heutigen Artikel geht:

Mit der ersten Regel geht es bereits recht speziell los:

T.60: Minimize a template’s context dependencies

Ich habe einige Momente benötigt, bis ich die Regel verstanden habe. Ein Blick auf die Funktions-Templates sort und algo hilft. Dies ist das vereinfachte Beispiel aus den Guidelines:

template<typename C>
void sort(C& c)
{
std::sort(begin(c), end(c)); // necessary and useful dependency
}

template<typename Iter>
Iter algo(Iter first, Iter last) {
for (; first != last; ++first) {
auto x = sqrt(*first); // potentially surprising dependency: which sqrt()?
helper(first, x); // potentially surprising dependency:
// helper is chosen based on first and x
}

Es wäre optimal, ist aber nicht immer erreichbar, dass ein Template lediglich seine Argumente verwendet. Dies gilt für das Funktions-Template sort; aber nicht für algo. Das Funktions-Template algo besitzt Abhängigkeiten zu den Funktionen sqrt und helper. Letztlich führt die Implementierung von algo mehr Abhängigkeiten ein, als es sich aus dem Interface ablesen lässt.

T.61: Do not over-parameterize members (SCARY)

Falls ein Mitglied eines Templates nicht von Template-Parametern abhängt, entferne es aus dem Template. Ein Mitglied kann ein Datentyp oder eine Methode sein. Indem du diese Regel anwendest, verringert sich die Codegröße, denn nichtgenerischer Code ist nicht Bestandteil des Templates.

Das Beispiel aus den Guidelines ist leicht zu verstehen:

template<typename T, typename A = std::allocator{}>
// requires Regular<T> && Allocator<A>
class List {
public:
struct Link { // does not depend on A
T elem;
T* pre;
T* suc;
};

using iterator = Link*;

iterator first() const { return head; }

// ...
private:
Link* head;
};

List<int> lst1;
List<int, My_allocator> lst2;

Der Datentyp Link hängt nicht vom Template-Parameter A ab. Daher kann ich ihn entfernen und in List2 direkt verwenden:

template<typename T>
struct Link {
T elem;
T* pre;
T* suc;
};

template<typename T, typename A = std::allocator{}>
// requires Regular<T> && Allocator<A>
class List2 {
public:
using iterator = Link<T>*;

iterator first() const { return head; }

// ...
private:
Link* head;
};

List<int> lst1;
List<int, My_allocator> lst2;

Dies war einfach? Ja? Nein? Die Regel verwendet die Abkürzung SCARY. Für was steht sie? Zumindest bin ich neugierig. Aber um ehrlich zu sein, kannst du die nächsten Zeilen ignorieren.

Das Akronym steht für "describes assignments and initializations that are Seemingly erroneous (appearing Constrained by conflicting generic parameters), but Actually work with the Right implementation (unconstrained bY the conflict due to minimized dependencies)".

Die Details dazu gibt es im Dokument N2911. Um dich nicht zu langweilen, hier ist die Idee vereinfacht auf die Container der Standard Template Libray angewandt: Diese besitzen keine Abhängigkeiten zu ihren Datentypen key_compare, hasher, key_equal oder allocator und können damit unabhängig erweitert werden.

Die nächste Regel hilft, um die Codegröße zu reduzieren.

T.62: Place non-dependent class template members in a non-templated base class

Lass es mich einfacher ausdrücken: Verschiebe die Funktionalität eines Templates, die nicht von den Template-Parametern abhängt, in eine Basisklasse, die kein Template ist.

Zu dieser Regel bieten die Guidelines ein einfaches Beispiel an:

template<typename T>
class Foo {
public:
enum { v1, v2 };
// ...
};

Die Aufzählung hängt nicht vom Typparameter T ab und sollte daher in eine nichtgenerische Basisklasse verschoben werden:

struct Foo_base {
enum { v1, v2 };
// ...
};

template<typename T>
class Foo : public Foo_base {
public:
// ...
};

Jetzt lässt sich Foo ohne Template-Argumente und Template-Instanziierung verwenden.

Diese Technik ist interessant, wenn du die Codegröße reduzieren willst. Dazu habe ich ein einfaches Klassen-Template Array implementiert:

// genericArray.cpp

#include <cstddef>
#include <iostream>

template <typename T, std::size_t N>
class Array{
public:
Array()= default;
std::size_t getSize() const{
return N;
}
private:
T elem[N];
};

int main(){

Array<int, 100> arr1;
std::cout << "arr1.getSize(): " << arr1.getSize() << std::endl;

Array<int, 200> arr2;
std::cout << "arr2.getSize(): " << arr2.getSize() << std::endl;

}

Bei genauer Betrachtung des Klassen-Templates Array fällt auf, dass die Methode getSize lediglich vom Typparameter N abhängt. Daher werde ich den Code refaktorieren und eine Klasse ArrayBase definieren, die nur vom Typparameter T abhängt:

// genericArrayInheritance.cpp

#include <cstddef>
#include <iostream>


template<typename T>
class ArrayBase {
protected:
ArrayBase(std::size_t n): size(n) {}
std::size_t getSize() const {
return size;
};
private:
std::size_t size;
};

template<typename T, std::size_t n>
class Array: private ArrayBase<T>{
public:
Array(): ArrayBase<T>(n){}
std::size_t getSize() const {
return ArrayBase<T>::getSize();
}
private:
T data[n];
};


int main(){

Array<int, 100> arr1;
std::cout << "arr1.getSize(): " << arr1.getSize() << std::endl;

Array<int, 200> arr2;
std::cout << "arr2.getSize(): " << arr2.getSize() << std::endl;

}

Array besitzt zwei Template-Parameter für den Datentyp T und die Länge n. Hingegen hat ArrayBase nur ein Template-Parameter für den Datentyp T. Arrray ist von ArrayBase abgeleitet. Das heißt, dass ArrayBase zwischen allen Instanzen von Array geteilt wird, die denselben Datentyp T verwenden. In dem konkreten Fall bedeutet dies, dass die getSize-Methode von Array die von ArrayBase verwendet.

Danke CppInsight kann ich den vom Compiler erzeugten Code direkt zeigen.

Hier ist die Instanziierung von ArrayBase<int>:

Und hier sind die Instanziierungen für Array<int, 100>:

und Array<int, 200>:

Wie geht's weiter?

Natürlich gibt es mehr Regel zu Template-Definitionen. Daher geht meine Geschichte zu Templates im nächsten Artikel weiter. Ich hoffe, dass meine Erläuterungen zu Templates ausreichend sind, denn ich weiß, dass viele Programmierer Templates nicht verwenden wollen. Für mich sind die zentralen Ideen von Templates einfach zu verstehen, aber ihre Syntax besitzt noch einiges an Verbesserungspotenzial.