Skip to content

MetaProgramming

ODR

在整个程序中,任何非内联函数或非模板类都只能有且只有一个定义。

ODR 对模板的例外

例外规则 : 模板函数和模板类的成员函数被视为拥有特殊的弱链接(Weak Linkage),或者像 inline 函数一样处理。

链接器的作用 : 当链接器看到多个 .cpp 文件都生成了相同的模板实例化代码(例如 std::vector<int>::push_back),它会根据这个弱链接属性,保留其中一个定义,而静默地丢弃所有重复的定义。

模板实例化模型问题

由于编译器必须在看到具体类型时才能生成代码,这就导致了一个实践上的困难:

模板的定义(.tpp.cpp 部分)不能与声明(.h 部分)完全分离。

显式实例化

  1. 集中生成 (在 .cpp 文件中): 在模板的定义文件(template.cpp)中,程序员使用 template class MyTemplate<int>; 语句。
  2. 效果: 编译器看到这个语句后,会立即生成 MyTemplate<int> 版本的机器码,并且只在这里生成一次
  3. 阻止重复 (在 .h 文件中): 在模板的头文件(template.h)中,程序员使用 extern template class MyTemplate<int>; 语句。
  4. 效果: 任何包含这个头文件的 .cpp 文件看到 extern template 后,编译器会得到指令:“这个模板实例的代码,你不需要生成,链接器会在其他地方找到它。”
template int add<int>(int, int);         // 显式实例化定义
template double add<double>(double, double);

比如

// add.h
template<typename T>
T add(T a, T b);

// add.cpp
#include "add.h"
template int add<int>(int, int);
template double add<double>(double, double);

暴露接口

// lib.h
template<typename T>
class Matrix {
    // ...
};
// 声明“我会提供这几种现成实例化”
extern template class Matrix<float>;
extern template class Matrix<double>;

入门

什么是模板元编程?

模板元编程(Template Metaprogramming)是在编译期用模板机制编写代码,用于自动生成最终可编译的 C++ 代码

  • 正常的代码在 运行时 执行;
  • 元编程是在 编译时 发生,生成新的代码。

用处:

  • 实现通用函数和类(支持多种类型)
  • 消除重复代码(如浮点版和整型版的函数)
  • 做到编译期检查、优化甚至计算
template <typename T>
auto pow_n(const T& v, int n) {
    auto product = T{1};
    for (int i = 0; i < n; ++i) {
        product *= v;
    }
    return product;
}

编译器行为: 每当你使用不同的类型 T(比如 float, int),编译器会为你自动生成一个版本:

auto x = pow_n<float>(2.0f, 3); // 生成 float 版本
auto y = pow_n<int>(3, 3);      // 生成 int 版本

编译器会为每种模板实例化 生成真正的函数/类

练习

编译期求递归的阶乘值

template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};
// 特化:N = 0,递归终止
template<>
struct Factorial<0> {
    static constexpr int value = 1;
};
std::println("{}", Factorial<2>::value);

Remove Const, Is same, enable_if

template<typename V>
struct RemoveConst { using type = V; };
template<typename V>
struct RemoveConst<const V> { using type = V;};
template<typename V> using RemoveConst_t = RemoveConst<V>::type;

void_t

可以接受若干个模板参数,返回void类型,实现 void_t<>, std里面有 std::void_t

template<typename...>
using myvoid = void;

add_r_reference

template<typename T>
struct add_r_reference {
    using type = T&&;
};
template<typename T>
struct add_r_reference<T&> {
    using type = T&;
};

模板也可以做限制和校验

template <int N, typename T>
auto const_pow_n(const T& v) {
    static_assert(N >= 0, "Exponent must be non-negative");
    ...
}

Type Traits(类型萃取)

type_traits 是 C++ 标准库中提供的一组 编译期工具类模板,用于检查或转换类型信息,全部在 <type_traits> 头文件里。

两种类型萃取

返回布尔/整型值的类型萃取

这些用来判断类型的某些特性(如是否是浮点数、是否是指针等):

  • C++17 引入了简洁写法:xxx_v
std::is_floating_point_v<float>  // true
std::is_same_v<int, int>         // true

使用 type traits 提升函数智能性

template <typename T>
auto sign_func(const T& v) -> int {
    if (std::is_unsigned_v<T>) {
        return 1;
    }
    return v < 0 ? -1 : 1;
}

编译器在编译时就知道 T 是不是 unsigned,可以直接“裁剪”掉分支:这就是类型萃取结合模板的强大之处:根据类型做出“静态分支优化”,无运行时开销!

或者

template<typename T>
auto print(T x) -> void {
    if constexpr(std::is_pointer_v<T>) {
        std::println("Pointer: {}", *x);
    } else {
        std::println("Value: {}", x);
    }
}

decltype:获取变量或表达式的类型

auto sign_func = [](const auto& v) -> int {
    using ReferenceType = decltype(v);                       // auto const&
    using ValueType = std::remove_reference_t<ReferenceType>; // 去引用得到原始类型

    if (std::is_unsigned_v<ValueType>) {
        return 1;
    }
    return v < 0 ? -1 : 1;
};

例子2

template <typename Range>
auto to_vector(const Range& r) {
    using IteratorType = decltype(r.begin());          // 容器的迭代器类型
    using ReferenceType = decltype(*IteratorType());   // 迭代器解引用的值类型(如 int&)
    using ValueType = std::decay_t<ReferenceType>;     // 去掉引用和 const(得到真正元素类型)

    return std::vector<ValueType>(r.begin(), r.end());
}

declvalue

是一个模板函数,可以返回它自己的类型,但是没有实际定义,用于编译的时候检查

template<typename T>
typename add_r_reference<T>::type mydeclval();

什么时候加typename

当你要描述一个 struct 的成员

struct s{
    using type = int;  
};
typename s::type x;

这里的 s::type 前面要加,来表示它是一个类型。

std::enable_if_t 条件启用函数(SFINAE)

Subsitution Failure is not an Error

你想让这个函数作用域所有的floating类型比如 float, double, long double

template <typename T>
auto interpolate(T left, T right, T power)
  -> std::enable_if_t<std::is_floating_point_v<T>, T> {
  return left + (right - left) * power;
}

检测类成员函数

C++17前写法

template<typename T, typename U = void> struct has_push_back : std::false_type{};
template<typename T> struct has_push_back<T, myvoid<
    decltype(
        mydeclval<T&>().push_back(
            mydeclval<typename T::value_type>()
        )
    )
>> : std::true_type{};
  • concept 是一个命名的布尔表达式
  • 检查类型T是否满足特定条件
  • 编译时求值,返回true/false
  • requires表达式检查代码是否有效:
template<typename T>
concept has_print_name = requires(T obj) {
    obj.print_name();
};

...
if constexpr(has_print_name<T>) {
    x.print_name();
}

constexpr 编译期函数

当所有参数是编译期常量时,它会在编译时执行。

constexpr auto sum(int x, int y, int z) {
  return x + y + z;
}
const auto value = std::integral_constant<int, sum(1, 2, 3)>;
// 如果不是constexpr,编译报错

if constexpr 编译期条件分支

template <typename Animal>
auto speak(const Animal& a) {
  if constexpr (std::is_same_v<Animal, Bear>) a.roar();
  else if constexpr (std::is_same_v<Animal, Duck>) a.quack();
}

编译期 vs 运行时 多态性对比

编译期多态(模板 + if constexpr):

  • 类型信息在编译时已知,无需虚表,开销小,内联优化能力强
  • 使用场景:追求性能,类型已知。

运行时多态(虚函数 + 多态继承):

  • 类型信息运行时才知道,有虚表开销
  • 使用场景:类型不确定或接口抽象统一。

std::tuple —— 固定大小的异构容器

auto tpl = std::make_tuple(42, std::string{"hi"}, true);
auto i = std::get<0>(tpl);           // int
auto str = std::get<1>(tpl);         // std::string
auto flag = std::get<2>(tpl);        // bool

为什么 std::tuple 不能用 range-based for 循环?

因为 std::tuple异构容器,不同于 std::vector 等同构容器,它的每个元素可能是不同类型。而 for (const auto& v : tuple)v 的类型只能是一个确定的类型,无法适配多个类型,编译器在编译期就会报错。另外,std::tuple 也没有 begin()/end(),因此也不能用算法如 std::for_each

如何“遍历”一个 std::tuple

通过 模板元编程+递归调用+索引展开(index sequence) 实现编译期“展开”。

template <size_t Index, typename Tuple, typename Functor>
void tuple_at(const Tuple& tpl, const Functor& func) {
    func(std::get<Index>(tpl));
}

template <typename Tuple, typename Functor, size_t Index = 0>
void tuple_for_each(const Tuple& tpl, const Functor& func) {
    if constexpr (Index < std::tuple_size_v<Tuple>) {
        tuple_at<Index>(tpl, func);
        tuple_for_each<Tuple, Functor, Index + 1>(tpl, func);
    }
}

auto tpl = std::make_tuple(1, true, std::string{"Jedi"});
tuple_for_each(tpl, [](const auto& v) { std::cout << v << " "; });

如何实现类似 any_of 的操作?

template <typename Tuple, typename Functor, size_t Index = 0>
bool tuple_any_of(const Tuple& tpl, const Functor& f) {
    if constexpr (Index < std::tuple_size_v<Tuple>) {
        return f(std::get<Index>(tpl)) ? true :
               tuple_any_of<Tuple, Functor, Index + 1>(tpl, f);
    } else {
        return false;
    }
}

结构化绑定(C++17)简化 tuple 解包

auto [name, id, license] = std::make_tuple("James", 7, true);

for (auto&& [name, id, license] : agents) {
    std::cout << name << ", " << id << ", " << license << '\n';
}

用结构体代替 tuple 返回值(更可读)

auto make_bond() {
    struct Agent { std::string name; int id; bool license; };
    return Agent{"James", 7, true};
}

变参模板(Variadic Templates)

使函数可以接收任意数量参数

template <typename ...Ts>
auto expand_pack(const Ts& ...values) {
    auto tuple = std::tie(values...);
}
// 变参模板在编译时会展开成:
expand_pack(42, std::string("hi"));
 auto tuple = std::tie(42, "hi");

将变参模板转为字符串(结合 tuple_for_each)

template <typename ...Ts>
auto make_string(const Ts& ...values) {
    std::ostringstream sstr;
    auto tuple = std::tie(values...);
    tuple_for_each(tuple, [&sstr](const auto& v){ sstr << v; });
    return sstr.str();
}
auto s = make_string(1, "abc", true);  // → "1abctrue"

动态大小 异构类型

方法一:std::any(C++17 起)

std::vector<std::any> container{42, "hi", true};
std::cout << a; // ❌ 不知道 std::any 里是什么类型,编译失败

for (const auto& a : container) {
    if (a.type() == typeid(int)) {
        std::cout << std::any_cast<int>(a);
    } else if (a.type() == typeid(const char*)) {
        std::cout << std::any_cast<const char*>(a);
    } // ...
}

方法二:std::variant(C++17 起)

using VariantType = std::variant<int, std::string, bool>;
VariantType v = 7;
v = std::string{"Bjarne"};
v = false;

它是一个“受限类型的类型联合体”。

std::tuple 不同,它一次只存一个值,但类型由一个 固定列表指定。

编译器知道它可能的类型组合,无需手动判断类型,使用 std::visit 自动分发。

std::visit([](const auto& v) {
    std::cout << v;
}, v);

编译器会自动为每个可能的类型生成对应的 operator() 重载,如:

struct FunctorImpl {
    void operator()(const int& v) { std::cout << v; }
    void operator()(const std::string& v) { std::cout << v; }
    void operator()(const bool& v) { std::cout << v; }
};

实际上,std::visit 做的就是一个 type-switch:

构建动态大小、异构类型容器

using VariantType = std::variant<int, std::string, bool>;
std::vector<VariantType> container;
container.push_back(false);
container.push_back(std::string{"I am a string"});
container.push_back(13);

遍历

for (const auto& val : container) {
    std::visit([](const auto& v){ std::cout << v << '\n'; }, val);
}
int count = std::count_if(container.begin(), container.end(), [](const auto& v){
    return std::holds_alternative<bool>(v);
});

bool contains = std::any_of(container.begin(), container.end(), [](const auto& v){
    return std::holds_alternative<std::string>(v) &&
           std::get<std::string>(v) == "needle";
});

反射

什么是反射(Reflection)?

“反射”是指程序在运行时可以“查看”或“操作”自己的结构,比如成员变量、类型信息等。

C++ 不支持原生反射,所以我们要“伪造”一个。

问题:如何让类暴露出它的成员变量?

方法:实现一个 reflect() 成员函数,返回 std::tie(...) 构成的 std::tuple 引用。

class Town {
public:
    Town(size_t houses, size_t settlers, const std::string& name)
        : houses_{houses}, settlers_{settlers}, name_{name} {}

    auto reflect() const { return std::tie(houses_, settlers_, name_); }

private:
    size_t houses_{};
    size_t settlers_{};
    std::string name_{};
};

std::tie 会构造一个 std::tuple 的引用。

这样我们可以操作、比较、访问成员变量,就像处理 tuple 一样。

如何使用 reflect()

🎯 目标:自动实现如下功能:

  • 比较(==, !=, <)
  • 输出(重载 operator<<
auto operator==(const Town& t) const {
    return reflect() == t.reflect();
}

auto& operator<<(std::ostream& os, const Town& t) {
    tuple_for_each(t.reflect(), [&os](const auto& val){
        os << val << " ";
    });
    return os;
}

提升:用元编程自动化支持这些功能

🛠 判断类是否 reflect() 可用:

利用 std::experimental::is_detected 判断类是否有 reflect() 方法:

template <typename T>
using has_reflect_member = decltype(&T::reflect);

template <typename T>
constexpr bool is_reflectable_v = std::experimental::is_detected<has_reflect_member, T>::value;

基于此,自动启用操作符重载:

template <typename T, bool B = is_reflectable_v<T>>
auto operator==(const T& a, const T& b) -> std::enable_if_t<B, bool> {
    return a.reflect() == b.reflect();
}

完整

class Town {
public:
    Town(size_t h, size_t s, std::string n) : houses_(h), settlers_(s), name_(n) {}
    auto reflect() const { return std::tie(houses_, settlers_, name_); }

private:
    size_t houses_;
    size_t settlers_;
    std::string name_;
};

// 全局支持:自动提供 ==, !=, <, << 等
template <typename T, bool B = is_reflectable_v<T>>
auto operator==(const T& a, const T& b) -> std::enable_if_t<B, bool> {
    return a.reflect() == b.reflect();
}

template <typename T, bool B = is_reflectable_v<T>>
auto operator<<(std::ostream& os, const T& t) -> std::enable_if_t<B, std::ostream&> {
    tuple_for_each(t.reflect(), [&os](const auto& m){ os << m << " "; });
    return os;
}

这套“伪反射”机制虽然比不上Java/C#的内建反射强大,但在 C++ 中非常实用。它是一种利用元编程提升代码复用性和可维护性的典范,广泛用于:

  • JSON/XML 序列化
  • 数据库 ORM 映射
  • UI 数据绑定
  • 自动日志/诊断系统
  • 泛型算法适配器

安全泛型类型转换

为什么需要 safe_cast

在 C++ 中使用 static_castreinterpret_castdynamic_castconst_cast 时,可能会发生以下问题:

  1. 精度丢失:比如 doublefloat,或者 int64_tint32_t
  2. 符号混淆:比如负数转无符号类型;
  3. 指针类型转换错误:不同类型的指针强转可能导致 UB(未定义行为);
  4. 指针转整数:只有 uintptr_t/intptr_t 是合法的;
  5. float -> float 的截断溢出(如 1e39 转 float 得到 inf);

因此,我们希望有一种方法:

  • 调试模式 下执行运行时检查;
  • 发布模式 下快速转换;
  • 类型不合法时 编译报错(而不是运行时爆炸)。
template <typename T> constexpr auto make_false() { return false; }

template <typename Dst, typename Src>
auto safe_cast(const Src& v) -> Dst {
  using namespace std;

  constexpr auto is_same_type         = is_same_v<Src, Dst>;
  constexpr auto is_pointer_to_pointer = is_pointer_v<Src> && is_pointer_v<Dst>;
  constexpr auto is_float_to_float     = is_floating_point_v<Src> && is_floating_point_v<Dst>;
  constexpr auto is_number_to_number   = is_arithmetic_v<Src> && is_arithmetic_v<Dst>;
  constexpr auto is_intptr_to_ptr      = (is_same_v<uintptr_t, Src> || is_same_v<intptr_t, Src>) && is_pointer_v<Dst>;
  constexpr auto is_ptr_to_intptr      = is_pointer_v<Src> && (is_same_v<uintptr_t, Dst> || is_same_v<intptr_t, Dst>);

  if constexpr (is_same_type) {
    return v;
  }
  else if constexpr (is_intptr_to_ptr || is_ptr_to_intptr) {
    return reinterpret_cast<Dst>(v);
  }
  else if constexpr (is_pointer_to_pointer) {
    assert(dynamic_cast<Dst>(v) != nullptr);  // 确保能安全 downcast
    return static_cast<Dst>(v);
  }
  else if constexpr (is_float_to_float) {
    auto casted = static_cast<Dst>(v);
    auto casted_back = static_cast<Src>(casted);
    assert(!isnan(casted_back) && !isinf(casted_back));
    return casted;
  }
  else if constexpr (is_number_to_number) {
    auto casted = static_cast<Dst>(v);
    auto casted_back = static_cast<Src>(casted);
    assert(casted == casted_back);  // 确保无精度损失
    return casted;
  }
  else {
    static_assert(make_false<Src>(), "safe_cast(): Unsupported cast");
    return Dst{};
  }
}

编译期字符串哈希

目标

避免运行时重复对字符串计算哈希,提高性能。

std::unordered_map 中,key 是 std::string 时,每次查找都要重新计算哈希。而如果我们能在编译期就计算好哈希值,可以显著优化:

constexpr auto hash_function(const char* str) -> size_t {
  size_t sum = 0;
  for (auto p = str; *p != '\0'; ++p) {
    sum += *p;
  }
  return sum;
}

实现 PrehashedString

class PrehashedString {
public:
  template <size_t N>
  constexpr PrehashedString(const char (&str)[N])
      : hash_{hash_function(str)},
        size_{N - 1},  // 不包括 null terminator
        strptr_{str} {}

  auto operator==(const PrehashedString& other) const {
    return size_ == other.size_ &&
           std::equal(c_str(), c_str() + size_, other.c_str());
  }

  auto get_hash() const { return hash_; }
  auto c_str() const -> const char* { return strptr_; }

private:
  size_t hash_;
  size_t size_;
  const char* strptr_;
};

并在 std 命名空间里自定义 std::hash

namespace std {
template <>
struct hash<PrehashedString> {
  constexpr size_t operator()(const PrehashedString& s) const {
    return s.get_hash();
  }
};
}

编译期验证示例

auto test() {
  const auto& s = PrehashedString("abc");
  return std::hash<PrehashedString>{}(s);
}
// 编译期就确定 hash = 294,汇编中出现 .quad 294