Μετάβαση στο περιεχόμενο

Εξειδίκευση προτύπων & αποστολή ετικετών

Κατακτήστε την πλήρη και μερική εξειδίκευση, μάθετε πότε να προτιμάτε την υπερφόρτωση και ανακαλύψτε την αποστολή ετικετών και εάν constexpr ως εναλλακτικές λύσεις για τη διακλάδωση στο χρόνο μεταγλώττισης.

Γιατί να ειδικευτείτε;

Το πρωτεύον πρότυπο παρέχει μια γενική υλοποίηση που λειτουργεί για τους περισσότερους τύπους. Αλλά μερικές φορές ένας συγκεκριμένος τύπος χρειάζεται ριζικά διαφορετική μεταχείριση. Για παράδειγμα, τοstd::vectorείναι μια εξειδίκευση που συσκευάζει bools σε bits για απόδοση χώρου (αν και θεωρείται ευρέως ως λάθος στην τυπική βιβλιοθήκη).

Η Ειδίκευση προτύπων σάς επιτρέπει να παρέχετε έναν εναλλακτικό ορισμό για συγκεκριμένα ορίσματα προτύπου. Υπάρχουν δύο είδη:

1. Πλήρης (σαφής) εξειδίκευση — διορθώνετε όλες τις παραμέτρους προτύπου σε συγκεκριμένους τύπους.
2. Μερική εξειδίκευση — διορθώνετε ορισμένες παραμέτρους ή τις περιορίζετε (π.χ. εξειδίκευση για όλους τους τύπους δεικτών). Η μερική εξειδίκευση είναι διαθέσιμη μόνο για πρότυπα τάξης, όχι για πρότυπα συναρτήσεων.

Full Specialization

A full specialization provides a completely custom implementation for one specific set of template arguments. The syntax usestemplate <>(an empty parameter list) followed by the class or function with concrete arguments.

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
}

Partial Specialization

Partial specialization constrains some template parameters while leaving others generic. This is extremely powerful for matching families of types — all pointers, all arrays, all pairs, etc. Only class/struct/variable templates support partial specialization.

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
}
Παγίδα

Τα πρότυπα συναρτήσεων δεν μπορούν να είναι μερικώς εξειδικευμένα — μόνο πλήρως εξειδικευμένα. Ακόμη χειρότερα, οι εξειδικεύσεις προτύπων συναρτήσεων δεν συμμετέχουν στην ανάλυση υπερφόρτωσης με τον τρόπο που θα περίμενε κανείς. Ο μεταγλωττιστής επιλέγει πρώτα το καλύτερο πρότυπο βάσης μέσω ανάλυσης υπερφόρτωσης και μόνο στη συνέχεια ελέγχει εάν αυτό το βασικό πρότυπο έχει αντίστοιχη εξειδίκευση.

Αυτό οδηγεί σε εκπληκτική συμπεριφορά:

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 ή έννοιες.

Μοτίβο αποστολής ετικέτας

Η Αποστολή ετικετών χρησιμοποιεί κενούς τύπους δομής ("ετικέτες") για να επιλέξει υπερφορτώσεις κατά το χρόνο μεταγλώττισης. Η τυπική βιβλιοθήκη το χρησιμοποιεί εκτενώς — τοstd::advanceαποστέλλει σε ετικέτες κατηγορίας επαναλήπτη. Αυτή ήταν η κυρίαρχη τεχνική διακλάδωσης χρόνου μεταγλώττισης πριν από τοif 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: The Modern Alternative

C++17's if constexpr provides a cleaner way to branch at compile time within a single function. The discarded branch is not instantiated, so it does not need to compile for the given type. This largely replaces tag dispatch and SFINAE for simple cases.

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"
}
Βέλτιστη πρακτική

Χρησιμοποιήστε αυτόν τον οδηγό απόφασης για διακλάδωση χρόνου μεταγλώττισης:

- if constexpr(C++17) — απλούστερο, λειτουργεί σε μία μόνο λειτουργία, δεν χρειάζονται ειδικά μοτίβα. Προτιμήστε αυτό για απλή λογική βασισμένη στον τύπο.
- Έννοιες/απαιτήσεις (C++20) — το καλύτερο για τον περιορισμό υπερφόρτωσης συναρτήσεων ή ολόκληρων προτύπων. Τεκμηριώνεται και δίνει ξεκάθαρα μηνύματα λάθους.
- Αποστολή ετικετών — εξακολουθεί να είναι χρήσιμη όταν έχετε μια υπάρχουσα ιεραρχία ετικετών (όπως κατηγορίες επαναλήψεων). Οι βάσεις κωδικών πριν από το C++17 βασίζονται σε μεγάλο βαθμό σε αυτό.
- SFINAE /enable_if — η παλαιότερη τεχνική. Πιο δύσκολο στην ανάγνωση, κρυπτικά λάθη. Αποφύγετε τον νέο κώδικα εάν λειτουργούν οι έννοιες ή τοif constexpr.
- Πλήρης εξειδίκευση — χρήση για πρότυπα κλάσεων όταν χρειάζεστε μια εντελώς διαφορετική υλοποίηση για έναν συγκεκριμένο τύπο.
- Ποτέ να μην χρησιμοποιείτε την εξειδίκευση προτύπου συναρτήσεων — αντ' αυτού χρησιμοποιήστε υπερφόρτωση.

Βασικά συμπεράσματα
  • Full specialization (template <>) provides a custom implementation for one specific type
  • Partial specialization constrains some parameters — available for class templates only, not function templates
  • 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) είναι η σύγχρονη, πιο καθαρή εναλλακτική που απορρίπτει τα κλαδιά που δεν έχουν αφαιρεθεί κατά τη στιγμή της μεταγλώττισης

Κουίζ — Δοκιμάστε τις γνώσεις σας

(20 XP)

1. Why should you avoid specializing function templates?

2. Which types of templates support partial specialization?

3. Τι διαφορετικό κάνει το`if constexpr`από ένα κανονικό`if`;