Vollständige Spezialisierung von Funktions-Templates

Um nachzuvollziehen, warum überladene Funktionen grundsätzlich Funktions-Templates vorzuziehen sind, ist ein Verständnis Letzterer hilfreich.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Lesezeit: 8 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Um nachzuvollziehen, warum überladene Funktionen grundsätzlich Funktions-Templates vorzuziehen sind, ist ein Verständnis Letzterer hilfreich.

Wie ich in meinem vorherigen Artikel "Einführung in die Template-Spezialisierung" gezeigt habe, können Funktions-Templates nur vollständig, aber nicht teilweise spezialisiert werden. Um meine lange Geschichte kurz zu machen: Spezialisiere keine Funktions-Templates. Verwende das Überladen von Funktionen.

Das wirft die Frage auf, warum ich über ein Feature von C++ schreibe, das man nicht benutzen sollte. Der Grund ist ganz einfach: We das überraschende Verhalten von voll spezialisierten Funktions-Templates sieht, wird wohl eine nicht-generische Funktion vorziehen.

Dieser Titel ist aus den C++ Core Guidelines: T.144: Don't specialize function templates

Der Grund für diese Regel ist einfach: Die Spezialisierung von Funktions-Templates nimmt nicht am Überladen teil. Schauen wir mal, was das bedeutet. Mein Programm basiert auf dem Programmschnipsel von Dimov/Abrahams.

// dimovAbrahams.cpp

#include <iostream>
#include <string>

// getTypeName

template<typename T> // (1) primary template
std::string getTypeName(T){
return "unknown";
}

template<typename T> // (2) primary template that overloads (1)
std::string getTypeName(T*){
return "pointer";
}

template<> // (3) explicit specialization of (2)
std::string getTypeName(int*){
return "int pointer";
}

// getTypeName2

template<typename T> // (4) primary template
std::string getTypeName2(T){
return "unknown";
}

template<> // (5) explicit specialization of (4)
std::string getTypeName2(int*){
return "int pointer";
}

template<typename T> // (6) primary template that overloads (4)
std::string getTypeName2(T*){
return "pointer";
}

int main(){

std::cout << std::endl;

int *p;

std::cout << "getTypeName(p): " << getTypeName(p) << std::endl;
std::cout << "getTypeName2(p): " << getTypeName2(p) << std::endl;

std::cout << std::endl;

}

Der Code sieht nur auf den ersten Blick ziemlich langweilig aus. In (1) wird das primäre Template getTypeName definiert. (2) ist eine Überladung für Zeiger und (3) eine vollständige Spezialisierung für einen int-Zeiger. Im Fall von getTypeName2 habe ich eine kleine Variation vollzogen. Die explizite Spezialisierung (5) wird vor der Überladung für Zeiger (6) definiert.

Diese Umstellung der Funktionsaufrufe hat überraschende Konsequenzen.

Im ersten Fall wird die vollständige Spezialisierung für den int-Zeiger aufgerufen, im zweiten Fall die Überladung für Zeiger. Der Grund für dieses wenig intuitive Verhalten ist, dass Überladung die Spezialisierung von Funktions-Templates ignoriert. Überladung betrachtet nur primäre Templates und Funktionen. In beiden Fällen hat die Überladung beide primären Templates gefunden. Im ersten Fall (getTypeName) ist die Zeigervariante die passendere Version und daher wurde die explizite Spezialisierung für den int-Zeiger gewählt. In der zweiten Variante (getTypeName2) wurde die Zeigervariante gewählt. Die vollständige Spezialisierung bezieht sich auch auf das primäre Template (Zeile 4). Folglich wurde sie ignoriert.

Das war ziemlich kompliziert, aber es genügt, folgende Regel im Hinterkopf zu behalten: Spezialisiere keine Funktions-Templates. Verwende stattdessen nicht-generische Funktionen.

Hier ist der Beweis für die Aussage: Wenn man aus der expliziten Spezialisierung in (3) und (5) nicht-generische Funktionen macht, ist das Problem gelöst. Ich muss nur noch die Template-Deklaration template<> auskommentieren. Der Einfachheit halber habe ich die weiteren Kommentare entfernt.

// dimovAbrahams.cpp

#include <iostream>
#include <string>

// getTypeName

template<typename T>
std::string getTypeName(T){
return "unknown";
}

template<typename T>
std::string getTypeName(T*){
return "pointer";
}

// template<> // (3)
std::string getTypeName(int*){
return "int pointer";
}

// getTypeName2

template<typename T>
std::string getTypeName2(T){
return "unknown";
}

// template<> // (5)
std::string getTypeName2(int*){
return "int pointer";
}

template<typename T>
std::string getTypeName2(T*){
return "pointer";
}

int main(){

std::cout << std::endl;

int *p;

std::cout << "getTypeName(p): " << getTypeName(p) << std::endl;
std::cout << "getTypeName2(p): " << getTypeName2(p) << std::endl;

std::cout << std::endl;

}

Jetzt funktioniert die Überladung wie erwartet und die nicht-generische Funktion, die einen int-Zeiger nimmt, wird verwendet.

Ich habe bereits über Template Arguments geschrieben. Aber ich habe eine wichtige Tatsache vergessen: Template-Argumente von Funktions- oder Klassen-Templates können Defaultwerte besitzen.

Was ist den Klassen-Templates der Standard Template Library (STL) gemein? Viele der Template-Argumente besitzen Defaultwerte.

Hier sind ein paar Beispiele.

template<
typename T,
typename Allocator = std::allocator<T>
> class vector;

template<
typename Key,
typename T,
typename Hash = std::hash<Key>,
typename KeyEqual = std::equal_to<Key>,
typename Allocator = std::allocator< std::pair<const Key, T>>
> class unordered_map;

template<
typename T,
typename Allocator = std::allocator<T>
> class deque;

template<
typename T,
typename Container = std::deque<T>
> clas Stack;

template<
typename CharT,
typename Traits = std::char_traits<CharT>,
typename Allocator = std::allocator<CharT>
> class basic_string;

Dies ist eine große Stärke der STL:

  • Jeder Container hat einen Standard-Allokator, der von seinen Elementen abhängt.
  • Für eine std::unordered_map muss man lediglich die notwendigen Argumente wie den Schlüsseltyp und den Wertetyp angeben: std::unordered_map<std::string, int>.
  • Eine std::unordered_map lässt sich auch instanziieren, indem man eine spezielle Hash-Funktion und ein spezielles binäres Prädikat verwendet. Die Hash-Funktion muss den Hash-Wert für den Schlüssel zurückgeben und das spezielle binäres Prädikat muss bestimmen, ob zwei Schlüssel gleich sind: std::unordered_map<std::string, int, MyHash>, oder auch std::unordered_map<std::string, int, MyHash, MyBinaryPredicate>.
  • std::string ist nur ein Alias für einen allgemeinen Zeichentyp. Dies sind die Aliase basierend auf std::basic_string.

std::string std::basic_string<char>
std::wstring std::basic_string<wchar_t>
std::u8string std::basic_string<char8_t> (C++20)
std::u16string std::basic_string<char16_t> (C++11)
std::u32string std::basic_string<char32_t> (C++11)

Wenn ein Template-Argument einen Defaultwert besitzt, müssen die folgenden Template-Argumente auch Defaultwerte besitzen.

Bis jetzt habe ich nur über Defaultwerte von Klassen-Templates geschrieben. Ich möchte diesen Beitrag mit einem Beispiel zu Funktions-Templates beenden.

Angenommen, ich möchte für ein paar Objekte des gleichen Typs entscheiden, welches kleiner ist. Ein Algorithmus wie isSmaller modelliert eine universelle Idee und sollte daher ein Template sein.

// templateDefaultArguments.cpp

#include <functional>
#include <iostream>
#include <string>

class Account{
public:
explicit Account(double b): balance(b){}
double getBalance() const {
return balance;
}
private:
double balance;
};

template <typename T, typename Pred = std::less<T>> // (1)
bool isSmaller(T fir, T sec, Pred pred = Pred() ){
return pred(fir,sec);
}

int main(){

std::cout << std::boolalpha << '\n';

std::cout << "isSmaller(3,4): " << isSmaller(3,4) << '\n'; // (2)
std::cout << "isSmaller(2.14,3.14): " << isSmaller(2.14,3.14) << '\n';
std::cout << "isSmaller(std::string(abc),std::string(def)): " <<
isSmaller(std::string("abc"),std::string("def")) << '\n';

bool resAcc= isSmaller(Account(100.0),Account(200.0), // (3)
[](const Account& fir, const Account& sec){
return fir.getBalance() < sec.getBalance(); });
std::cout << "isSmaller(Account(100.0),Account(200.0)): "
<< resAcc << '\n';

bool acc= isSmaller(std::string("3.14"),std::string("2.14"), // (4)
[](const std::string& fir, const std::string& sec){
return std::stod(fir) < std::stod(sec); });
std::cout << "isSmaller(std::string(3.14),std::string(2.14)): "
<< acc << '\n';

std::cout << '\n';

}

Im Standardfall (2) funktioniert isSmaller wie erwartet. isSmaller (1) verwendet das Template-Argument std::less, das eines von vielen vordefinierten Funktionsobjekten in der STL ist. Es wendet den kleiner-als Operator < auf seine Argumente an. Um ihn einzusetzen, muss ich std::less<T> in der folgenden Zeile instanziieren: Pred pred = Pred().

Dank des Standardarguments für pred kann ich Strings (4) vergleichen. Account unterstützt den less-than-Operator nicht. Trotzdem ist es mir möglich, Accounts zu vergleichen (3). Außerdem möchte ich Strings nicht lexikografisch, sondern basierend auf ihrer internen Zahl vergleichen (4). Durch die Bereitstellung der beiden Lambda-Ausdrücke in (3) und (4) als binäres Prädikat kann ich die Aufgabe erfolgreich erledigen.

Wenn du dir die Grafik am Anfang dieses Beitrags ansiehst, siehst du, dass ich mit den Grundlagen von Templates fertig bin. In meinem nächsten Artikel über Templates tauche ich in ihre Details weiter ein und schreibe über die Template-Instanziierung. ()