VollstÀndige Spezialisierung von Funktions-Templates
Um nachzuvollziehen, warum ĂŒberladene Funktionen grundsĂ€tzlich Funktions-Templates vorzuziehen sind, ist ein VerstĂ€ndnis Letzterer hilfreich.
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 [1]" 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.
Funktions-Templates sollen nicht spezialisiert werden
Dieser Titel ist aus den C++ Core Guidelines: T.144: Don't specialize function templates [2]
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.
Default Template-Argumente
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_mapmuss man lediglich die notwendigen Argumente wie den SchlĂŒsseltyp und den Wertetyp angeben:std::unordered_map<std::string, int>. - Eine
std::unordered_maplĂ€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 auchstd::unordered_map<std::string, int, MyHash, MyBinaryPredicate>. std::stringist nur ein Alias fĂŒr einen allgemeinen Zeichentyp. Dies sind die Aliase basierend aufstd::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.
Wie geht's weiter?
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. ( [3])
URL dieses Artikels:
https://www.heise.de/-6124700
Links in diesem Artikel:
[1] https://heise.de/-6118187
[2] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-specialize-function
[3] mailto:rainer@grimm-jaud.de
Copyright © 2021 Heise Medien