跳至内容

模板特化与标签分发

掌握全特化和偏特化,了解何时应优先使用重载,并发现标签分发和 if constexpr 作为编译时分支的替代方案。

为什么需要特化?

主模板提供了适用于大多数类型的泛型实现。但有时某个特定类型需要根本不同的处理方式。例如,std::vector 是一个将 bool 压缩为位以节省空间的特化(尽管它被广泛认为是标准库中的一个设计失误)。

模板特化让你为特定的模板参数提供替代定义。有两种类型:

1. 全(显式)特化 ── 你将所有模板参数固定为特定类型。
2. 偏特化 ── 你固定部分参数或对其进行约束(例如,为所有指针类型特化)。偏特化仅适用于类模板,不适用于函数模板。

全特化

全特化为一组特定的模板参数提供完全自定义的实现。语法使用 template <>(空参数列表),后跟带有具体参数的类或函数。

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
}

偏特化

偏特化约束部分模板参数,同时保留其他参数为泛型。这对于匹配类型族非常强大 ── 所有指针、所有数组、所有 pair 等。只有类/结构体/变量模板支持偏特化。

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 - 主模板 */ }
template <typename T> void f(T*)  { /* #2 - 指针重载 */ }
template <> void f<int*>(int*)     { /* #3 - #1 的特化 */ }

调用 f((int*)nullptr) 会选择 #2(更好的重载匹配),而不是 #3(它特化的是 #1 而非 #2)。解决方法:使用普通函数重载而非函数模板特化。如果你需要编译时类型分支,使用 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:现代替代方案

C++17 的 if constexpr 提供了一种更简洁的方式,可以在单个函数内进行编译时分支。被丢弃的分支不会被实例化,因此它不需要为给定类型编译通过。这在很大程度上取代了简单场景中的标签分发和 SFINAE。

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) ── 最简单,在单个函数内工作,不需要特殊模式。对于简单的基于类型的逻辑优先使用。
- 概念/requires(C++20) ── 最适合约束函数重载或整个模板。自文档化且给出清晰的错误消息。
- 标签分发 ── 当你有现成的标签层次结构(如迭代器类别)时仍然有用。C++17 之前的代码库大量依赖此技术。
- SFINAE / enable_if ── 最古老的技术。难以阅读,错误信息晦涩。如果概念或 if constexpr 可用,在新代码中应避免使用。
- 全特化 ── 当你需要为某个特定类型提供完全不同的实现时,用于类模板。
- 永远不要使用函数模板特化 ── 使用重载代替。

关键要点
  • 全特化(template <>)为一种特定类型提供自定义实现
  • 偏特化约束部分参数 ── 仅适用于类模板,不适用于函数模板
  • 函数模板特化容易出错 ── 优先使用重载或 if constexpr
  • 标签分发使用空结构体类型通过 iterator_traits 在编译时选择重载
  • if constexpr(C++17)是现代的、更简洁的替代方案,它在编译时丢弃未采用的分支

测验 — 检验你的知识

(20 经验值)

1. 为什么应该避免特化函数模板?

2. 哪些类型的模板支持偏特化?

3. `if constexpr` 与普通 `if` 有什么不同?