C++ Insights: Template-Instanziierung

C++ Insights hilft, den automatischen Prozess der Template-Instanziierung besser zu verstehen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 7 Min.
Von
  • Andreas Fertig
  • Rainer Grimm
Inhaltsverzeichnis

In Andreas Fertigs heutigem Artikel geht es um die Template-Instanziierung. C++ Insights hilft, diesen automatischen Prozess besser zu verstehen.

Die Zukunft von C++ spricht Templates. Daher ist es eine sehr gute Idee, dein Wissen über Templates zu vertiefen.

Ich möchte an dieser Stelle mit einem Haftungsausschluss beginnen. Es gibt andere Werkzeuge, um diese Arbeit zu erledigen. Ich habe eine Vorschau von Visual Studio gesehen, in der wir das instanziierte Template anzeigen können. Gleiches gilt für Cevelop. Es ist keine einzigartige Funktion, die C++ Insights hier bietet. Mit Ausnahme eines Unterschieds werden hier die Transformationen für den gesamten Code angezeigt, den wir auf einmal eingeben. Alles! Nicht nur Templates.

Worüber ich spreche, ist eine Situation, von der ich glaube, dass sie viele von uns mindestens einmal erlebt haben? Es gibt dieses Funktions-Template. Wir möchten gerne wissen, für welche Typen es instanziiert wird und woher. Eine einfache Sache für C++ Insights. Der Compiler muss dies wissen, ebenso C++ Insights.

In der Lage zu sein, den Code zu zeigen, der effektiv läuft, ist während des Unterrichts von Wert. Ich habe erfahren, dass es den Schülern sehr geholfen hat, wenn sie sehen können, was los ist.

Eine schöne Sache, die C++ Insights zeigt, ist, was es nicht zeigt. Der Compiler, zumindest Clang, in dem C++ Insights läuft, ist bestrebt, uns den effizientesten Code zu liefern. Bei Templates generiert der Compiler Code nur für Funktionen oder Methoden, die tatsächlich verwendet werden. Wir können ein Klassen-Template mit einer bestimmten Methode haben, die niemals aufgerufen wird. Wie hier:

template<typename T>
class Apple
{
public:
Apple() = default;

bool IsGreen() const { return false; }
bool IsRed() const { return true; }
};

int main()
{
Apple<int> apple;

if( apple.IsRed()) {}
}

In diesem Fall generiert der Compiler nicht den Methodenrumpf dieser Instantiierung (Apple <int>), wie wir in C++ Insights sehen können:

template<typename T>
class Apple
{
public:
Apple() = default;

bool IsGreen() const { return false; }
bool IsRed() const { return true; }
};

/* First instantiated from: insights.cpp:13 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Apple<int>
{
public:
// inline constexpr Apple() noexcept = default;
inline bool IsGreen() const;

inline bool IsRed() const;

// inline constexpr Apple(const Apple<int> &) = default;
// inline constexpr Apple(Apple<int> &&) = default;
};

#endif


int main()
{
Apple<int> apple = Apple<int>();
}

Selbst wenn die Methode mit einer anderen Instanziierung (Apple <char>) verwendet wird, gibt es keinen Code für die int-Variante. Natürlich ist die Methode für Apple <char> vorhanden. Überzeugen wir uns selbst in C++ Insights:

template<typename T>
class Apple
{
public:
Apple() = default;

bool IsGreen() const { return false; }
bool IsRed() const { return true; }
};

/* First instantiated from: insights.cpp:13 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Apple<int>
{
public:
// inline constexpr Apple() noexcept = default;
inline bool IsGreen() const;

inline bool IsRed() const;

// inline constexpr Apple(const Apple<int> &) = default;
// inline constexpr Apple(Apple<int> &&) = default;
};

#endif


/* First instantiated from: insights.cpp:14 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Apple<char>
{
public:
// inline constexpr Apple() noexcept = default;
inline bool IsGreen() const
{
return false;
}

inline bool IsRed() const;

// inline constexpr Apple(const Apple<char> &) = default;
// inline constexpr Apple(Apple<char> &&) = default;
};

#endif


int main()
{
Apple<int> apple = Apple<int>();
Apple<char> cApple = Apple<char>();
cApple.IsGreen();
}

Was wir auch mit C++ Insights sehen können, ist, welche Zeile im ursprünglichen Code die Instanziierung verursacht hat. Dies kann hilfreich sein, wenn wir eine bestimmte Instantiierung nicht erwarten.

Bei der Verwendung von C++ 17 und CTAD (Class Template Argument Deduction) kann es manchmal weniger offensichtlich sein, welche Typen man erhalten hat. Nehme diesen Code an (ich weiß, dass es wahrscheinlich leicht zu sehen ist):

#include <vector>

int main()
{
std::vector v{1,2,3};
std::vector vd{1.0,2.0,3.0};

//v = vd; // does not compile
}

Wir haben zwei std::vector, denen jeweils drei Nummern zugewiesen werden. Trotz der Tatsache, dass diese beiden Vektoren wirklich gleich aussehen, können wir vd nicht v zuweisen. Es ist wahrscheinlich offensichtlich, dass v vom Typ int ist, während vd vom Typ double ist. Eine einfache Sache für C++ Insights:

#include <vector>

int main()
{
std::vector<int, std::allocator<int> > v = std::vector<int, std::allocator<int> >{std::initializer_list<int>{1, 2, 3}, std::allocator<int>()};
std::vector<double, std::allocator<double> > vd = std::vector<double, std::allocator<double> >{std::initializer_list<double>{1.0, 2.0, 3.0}, std::allocator<double>()};
}

Dort kann man sehen, welchen Typ der Vektor wirklich hat.

Während wir über C++ 17 sprechen, gibt es eine weitere neue Funktion: constexpr if. Schauen wir uns an, was C++ Insights für uns dort leisten kann. Im folgenden Beispiel haben wir eine stringify-Vorlage, die aus dem an die Funktion übergebenen Parameter eine std::string erstellt:

#include <string>
#include <type_traits>

template <typename T>
std::string stringify(T&& t)
{
if constexpr(std::is_same_v<T, std::string>) {
return t;
} else {
return std::to_string(t);
}
}

int main()
{
auto x = stringify(2);
auto y = stringify(std::string{"Hello"});
}

Wenn wir einen std::string übergeben, wird dieser String natürlich zurückgegeben. Das constexpr if macht dieses Funktions-Template überhaupt erst möglich. Denn es gibt keine to_string-Funktion, die einen std::string entgegennimmt. Mit einem normalen if würde dieser Code nicht kompilieren.

Was passiert nun, wenn wir einen C-String übergeben? Wie hier:

#include <string>
#include <type_traits>

template <typename T>
std::string stringify(T&& t)
{
if constexpr(std::is_same_v<T, std::string>) {
return t;
} else {
return std::to_string(t);
}
}

int main()
{
auto x = stringify(2);
auto y = stringify("hello");
}

Es wird nicht kompiliert. Der Grund ist, dass es auch kein to_string für ein char-Array gibt. Wir können dies beheben, indem wir für diesen Fall ein zusätzliches if angeben:

#include <string>
#include <type_traits>

template <typename T>
std::string stringify(T&& t)
{
if constexpr(std::is_same_v<T, std::string>) {
return t;
} else if constexpr(std::is_array_v< std::remove_reference_t<T> >) {
return std::string{t};
} else {
return std::to_string(t);
}
}

int main()
{
auto x = stringify(2);
auto y = stringify("hello");
}

Jetzt kompiliert es. Was C++ Insights uns zeigt, sind die Template-Instanziierungen für die beiden Typen. Aber es gibt noch mehr. Es zeigt auch, welcher if-Zweig in dieser Instanziierung verwendet wird. Wenn wir genau hinsehen, können wir noch etwas anderes erkennen. C++ Insights zeigt uns auch, dass es in C++ kein else if gibt. Es gibt nur ein if und ein else. Warum ist das wichtig? Weil wir constexpr auf alle if-Zweige anwenden müssen. Ansonsten erhalten wir ein Laufzeit if in einem constexpr if else-Zweig.

Viel Spaß mit C++ Insights. Wer möchte, kann das Projekt unterstützen, entweder als Patreon oder natürlich auch mit Code-Beiträgen. Demnächst mehr zu C++ Insights zu Variadic Templates. ()