zurück zum Artikel

VollstÀndige Spezialisierung von Funktions-Templates

Rainer Grimm

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.

VollstÀndige Spezialisierung von Funktions-Templates

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 [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.

VollstÀndige Spezialisierung von Funktions-Templates

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.

VollstÀndige Spezialisierung von Funktions-Templates

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:

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.

VollstÀndige Spezialisierung von Funktions-Templates

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