Přeskočit na obsah

Specializace šablon a odeslání štítků

Osvojte si plnou a částečnou specializaci, naučte se, kdy preferovat přetížení, a objevte odesílání tagů a if constexpr jako alternativy pro větvení v době kompilace.

Proč se specializovat?

Primární šablona poskytuje obecnou implementaci, která funguje pro většinu typů. Někdy ale konkrétní typ potřebuje zásadně odlišné zacházení. Napříkladstd::vectorje specializace, která balí booly do bitů pro úsporu místa (ačkoli je to ve standardní knihovně považováno za chybu).

Specializace šablony vám umožňuje poskytnout alternativní definici pro konkrétní argumenty šablony. Existují dva druhy:

1. Plná (explicitní) specializace — všechny parametry šablony opravíte na konkrétní typy.
2. Částečná specializace – opravíte některé parametry nebo je omezíte (např. specializujete se na všechny typy ukazatelů). Částečná specializace je dostupná pouze pro šablony tříd, nikoli šablony funkcí.

Plná specializace

Úplná specializace poskytuje zcela vlastní implementaci pro jednu konkrétní sadu argumentů šablony. Syntaxe používátemplate <>(prázdný seznam parametrů) následovaný třídou nebo funkcí s konkrétními argumenty.

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

Částečná specializace

Částečná specializace omezuje některé parametry šablony, zatímco jiné nechává obecné. To je extrémně výkonné pro párování rodin typů — všechny ukazatele, všechna pole, všechny páry atd. Částečnou specializaci podporují pouze šablony tříd/struktur/proměnných.

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
Úskalí

Šablony funkcí nemohou být částečně specializované – pouze plně specializované. Horší je, že specializace šablon funkcí se ne podílejí na řešení přetížení tak, jak byste očekávali. Kompilátor nejprve vybere nejlepší základní šablonu pomocí rozlišení přetížení a teprve poté zkontroluje, zda má tato základní šablona odpovídající specializaci.

To vede k překvapivému chování:

template <typename T> void f(T)   { /* #1 - primary */ }
template <typename T> void f(T*)  { /* #2 - overload for pointers */ }
template <> void f<int*>(int*)     { /* #3 - specialization of #1 */ }

Calling f((int*)nullptr) selects #2 (the better overload match), not #3 (which specializes #1, not #2). The fix: use regular function overloading instead of function template specialization. If you need compile-time type branching, use if constexpr nebo koncepty.

Vzor odeslání značky

Odeslání značky používá prázdné typy struktur ("značky") k výběru přetížení v době kompilace. Standardní knihovna to široce využívá –std::advanceodesílá značky kategorie iterátoru. Toto byla dominantní technika větvení v době kompilace předif constexpr.

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

if constexpr: Moderní alternativa

if constexpr C++17 poskytuje čistší způsob větvení v době kompilace v rámci jediné funkce. Zahozená větev není konkretizována, takže ji není třeba kompilovat pro daný typ. To do značné míry nahrazuje odesílání štítků a SFINAE pro jednoduché případy.

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
Osvědčený postup

Pro větvení v době kompilace použijte tuto rozhodovací příručku:

- if constexpr(C++17) — nejjednodušší, funguje v rámci jediné funkce, nejsou potřeba žádné speciální vzory. Upřednostňujte to pro přímou logiku založenou na typu.
- Koncepty/požadavky (C++20) – nejlepší pro omezení přetížení funkcí nebo celých šablon. Samodokumentuje a poskytuje jasné chybové zprávy.
- Odeslání značky – stále užitečné, když máte existující hierarchii značek (jako kategorie iterátorů). Kódové základny pre-C++17 na tom silně spoléhají.
- SFINAE /enable_if — nejstarší technika. Hůře čitelné, záhadné chyby. Vyhněte se v novém kódu, pokud fungují koncepty neboif constexpr.
- Plná specializace — použijte pro šablony tříd, když potřebujete úplně jinou implementaci pro jeden konkrétní typ.
- Nikdy nepoužívejte specializaci šablony funkcí – místo toho použijte přetížení.

Klíčové poznatky
  • Full specialization (template <>) provides a custom implementation for one specific type
  • Částečná specializace omezuje některé parametry – dostupné pouze pro šablony tříd, nikoli šablony funkcí
  • Function template specialization is error-prone — prefer overloading orif constexpr
  • Tag dispatch uses empty struct types to select overloads at compile time viaiterator_traits
  • if constexpr(C++17) is the modern, cleaner alternative that discards untaken branches at compile time

Kvíz — Otestujte své znalosti

(20 XP)

1. Why should you avoid specializing function templates?

2. Které typy šablon podporují částečnou specializaci?

3. What does`if constexpr`do differently from a regular`if`?