MetaProgramming
ODR
在整个程序中,任何非内联函数或非模板类都只能有且只有一个定义。
ODR 对模板的例外
例外规则 : 模板函数和模板类的成员函数被视为拥有特殊的弱链接(Weak Linkage),或者像 inline 函数一样处理。
链接器的作用 : 当链接器看到多个 .cpp 文件都生成了相同的模板实例化代码(例如 std::vector<int>::push_back),它会根据这个弱链接属性,保留其中一个定义,而静默地丢弃所有重复的定义。
模板实例化模型问题
由于编译器必须在看到具体类型时才能生成代码,这就导致了一个实践上的困难:
模板的定义(
.tpp或.cpp部分)不能与声明(.h部分)完全分离。
显式实例化
- 集中生成 (在
.cpp文件中): 在模板的定义文件(template.cpp)中,程序员使用template class MyTemplate<int>;语句。 - 效果: 编译器看到这个语句后,会立即生成
MyTemplate<int>版本的机器码,并且只在这里生成一次。 - 阻止重复 (在
.h文件中): 在模板的头文件(template.h)中,程序员使用extern template class MyTemplate<int>;语句。 - 效果: 任何包含这个头文件的
.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_cast、reinterpret_cast、dynamic_cast、const_cast 时,可能会发生以下问题:
- 精度丢失:比如
double转float,或者int64_t转int32_t; - 符号混淆:比如负数转无符号类型;
- 指针类型转换错误:不同类型的指针强转可能导致 UB(未定义行为);
- 指针转整数:只有
uintptr_t/intptr_t是合法的; - 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