模板定制体系
概述
- 大型模板库必须解决一个核心问题
- 库作者写好了通用算法,但怎样允许用户为自己的类型提供特殊行为
C++中没有唯一的定制机制,而是形成了一组工具
| 机制 | 最适合表达 |
traits |
这个类型具有什么静态信息 |
policy |
这个组件采用哪一种行为策略 |
tag dispatch |
这次调用明确选择哪种实现 |
CRTP |
派生类向模板基类提供实现 |
ADL |
在类型所属命名空间中寻找操作 |
CPO |
对外提供统一、受控的定制入口 |
Concepts |
定制实现必须满足哪些要求 |
先建立总体模型
- 假设库提供
|
1 |
encode(encoder, message); |
- 库可以按照以下优先级寻找实现
- 这个统一入口就是
CPO - 背后的成员函数和
ADL是定制通道 Concepts控制通道是否合法
- 这个统一入口就是
Traits:把类型映射到静态信息
- 假设不同网络消息对应不同命令号
|
1 2 3 |
struct Login {}; struct Logout {}; struct Heartbeat {}; |
- 可以定义
traits
|
1 2 3 4 |
#include <cstdint> template<class Message> struct message_traits; |
- 为每种消息提供特化
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<> struct message_traits<Login> { static constexpr std::uint16_t command = 1; }; template<> struct message_traits<Logout> { static constexpr std::uint16_t command = 2; }; template<> struct message_traits<Heartbeat> { static constexpr std::uint16_t command = 3; }; |
- 通用代码
|
1 2 3 4 5 |
template<class Message> constexpr std::uint16_t command_of() { return message_traits<Message>::command; } |
Traits的核心模型是
|
1 2 3 |
输入一个类型 ↓ 得到相关类型、常量或属性 |
- 例如标准库
|
1 2 3 4 |
std::iterator_traits<Iterator>::value_type std::allocator_traits<Allocator>::pointer std::char_traits<char>::length(...) std::numeric_limits<int>::max() |
Traits可以包含什么Traits不一定只是std::true_type、std::false_type一类布尔判断
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 类型 template<class T> struct protocol_traits { using header_type = typename T::header_type; using size_type = std::uint32_t; }; // 常量 static constexpr bool supports_compression = true; // 函数 static std::size_t payload_size(const T& value); |
- 给
Traits增加Concept
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 未特化时,直接访问 message_traits<T>::command // 可能产生不清楚的错误。可以定义 template<class T> concept RegisteredMessage = requires { { message_traits<T>::command } -> std::convertible_to<std::uint16_t>; }; // 然后 template<RegisteredMessage Message> void send_message(const Message& message) { constexpr auto command = message_traits<Message>::command; // 编码 command 和 message } |
Traits的适用范围
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 适合 类型对应的协议号; allocator 相关类型; 容器或迭代器属性; 编译期尺寸和对齐; 序列化类型映射; 第三方类型无法修改,但允许提供外部特化 // 不适合 每个对象都不同的运行时状态; 需要运行时切换的策略; 复杂的动态多态行为 |
- 还有一个重要限制
- 对
traits的显式特化通常必须定义在主模板所属的命名空间中
- 对
Policy:把行为作为模板参数
Traits通常回答“类型是什么”,Policy通常回答“组件应该怎么做”- 例如协议解析失败时,可以有不同策略
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> #include <stdexcept> #include <string_view> struct ThrowOnError { void on_error(std::string_view message) const { throw std::runtime_error{ std::string{message} }; } }; struct LogOnError { void on_error(std::string_view message) const { std::cerr << "decode error: " << message << '\n'; } }; struct IgnoreError { void on_error(std::string_view) const noexcept { } }; |
- 组件接收策略类型
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
template<class ErrorPolicy = ThrowOnError> class Decoder { private: [[no_unique_address]] ErrorPolicy error_policy_; public: explicit Decoder( ErrorPolicy policy = {}) : error_policy_(std::move(policy)) { } void decode(bool valid) { if (!valid) { error_policy_.on_error( "invalid packet" ); } } }; |
|
1 2 3 |
Decoder<> strict_decoder; Decoder<LogOnError> logging_decoder; Decoder<IgnoreError> permissive_decoder; |
Policy的模型是- 组件算法保持不变
- 行为细节由模板参数注入
- 常见策略:
- 错误处理策略;
- 锁策略;
- 内存分配策略;
- 重试策略;
- 比较策略;
- 缓存淘汰策略;
- 日志策略;
- 序列化策略
- 为什么保存
Policy对象
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 如果策略完全无状态,也可以调用静态函数 ErrorPolicy::on_error(message); // 但保存对象可以支持有状态策略 struct RetryPolicy { int maximum_retries; bool should_retry(int attempt) const { return attempt < maximum_retries; } }; |
|
1 2 3 |
// C++20 使用 // 空策略通常不会额外增加对象大小,但具体布局仍由实现决定 [[no_unique_address]] ErrorPolicy error_policy_; |
Policy的代价
|
1 2 3 4 |
// 每种模板组合都会形成不同类型 Decoder<ThrowOnError> Decoder<LogOnError> Decoder<IgnoreError> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
优点: 1 编译期选择; 2 容易内联; 3 没有必然的虚调用; 4 可以针对策略优化。 代价: 1 编译时间增加; 2 代码体积可能增加; 3 不适合频繁运行时切换; 4 类型可能变得非常长; 5 过多策略组合会造成组合爆炸。 如果策略必须运行时频繁切换,可以考虑: 1 虚函数接口; 2 std::function; 3 std::variant; 4 手工函数指针表 |
Tag dispatch:用空类型选择实现
Tag是一个通常没有数据、只表达类别或意图的类型
|
1 2 3 4 5 6 7 8 9 10 |
struct blocking_t { }; struct nonblocking_t { }; inline constexpr blocking_t blocking{}; inline constexpr nonblocking_t nonblocking{}; |
- 通过重载选择实现
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void send_data( int socket, const char* data, std::size_t size, blocking_t) { // 循环发送直到全部完成 } void send_data( int socket, const char* data, std::size_t size, nonblocking_t) { // 尝试发送,处理 would-block } |
|
1 2 3 |
// Tag 的作用不是携带数据,而是让类型系统参与重载决议 send_data(sock, data, size, blocking); send_data(sock, data, size, nonblocking); |
Tag比布尔参数更清楚
|
1 2 3 4 |
// true 表示什么 send_data(sock, data, size, true); send_data(sock, data, size, blocking); |
- 类别
tag dispatch
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
template<class Iterator> void advance_impl( Iterator& iterator, std::ptrdiff_t distance, std::random_access_iterator_tag) { iterator += distance; } template<class Iterator> void advance_impl( Iterator& iterator, std::ptrdiff_t distance, std::input_iterator_tag) { while (distance-- > 0) { ++iterator; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<class Iterator> void advance( Iterator& iterator, std::ptrdiff_t distance) { using Category = typename std::iterator_traits< Iterator >::iterator_category; advance_impl( iterator, distance, Category{} ); } // 这里 traits 提供类别,tag dispatch 选择算法 |
|
1 2 3 4 5 6 7 8 |
// 现代 C++ 可以使用 Concept 重载或 if constexpr template<std::random_access_iterator Iterator> void advance(Iterator& iterator, std::ptrdiff_t distance) { iterator += distance; } |
CRTP:派生类作为模板参数
Curiously Recurring Template Pattern,奇异递归模板模式- 基本形式
|
1 2 3 4 5 6 7 8 |
template<class Derived> class Base { }; class Child : public Base<Child> { }; |
|
1 |
static_cast<Derived&>(*this) |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 访问派生类实现 #include <iostream> #include <string_view> template<class Derived> class MessageHandler { public: void dispatch(std::string_view data) { derived().on_message(data); } private: Derived& derived() { return static_cast<Derived&>(*this); } }; class LoginHandler : public MessageHandler<LoginHandler> { public: void on_message(std::string_view data) { std::cout << "login: " << data << '\n'; } }; |
|
1 2 |
LoginHandler handler; handler.dispatch("Aet"); |
CRTP与虚函数
CRTP |
虚函数 |
| 派生类型编译期已知 | 具体类型运行时确定 |
| 通常容易内联 | 通过虚表动态分发 |
| 不需要虚表 | 对象通常含虚表指针 |
| 类型之间强耦合 | 调用方可只依赖基类 |
| 不能自然存放异质派生对象 | 可通过基类指针统一存放 |
| 改变模板实现可能引发重新编译 | 更适合稳定动态接口 |
CRTP的风险CRTP依赖一个约定:Derived必须是真正的派生类型
|
1 2 3 4 5 6 7 8 9 |
// 如果写错继承关系 class Wrong : public MessageHandler<LoginHandler> { }; // 基类内部 static_cast<LoginHandler&>(*this) // 并不对应真实对象,使用结果可能是未定义行为 |
ADL:让操作跟着类型走
Argument-Dependent Lookup,实参依赖查找- 它会根据函数实参的类型,到关联命名空间中查找候选函数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> namespace protocol { struct Login { }; void encode(const Login&) { std::cout << "encode Login\n"; } } template<class T> void write_message(const T& message) { encode(message); } |
|
1 2 |
protocol::Login login; write_message(login); |
swap的经典写法
|
1 2 3 4 5 6 7 8 9 10 |
// 泛型代码不应该直接写 std::swap(left, right); // 因为限定调用会阻止 ADL 为用户类型找到更合适的 swap // 经典写法 using std::swap; swap(left, right); // 它同时提供: // 普通查找找到 std::swap 作为后备; // ADL 找到类型命名空间中的专用 swap |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace app { struct Buffer { }; void swap(Buffer& left, Buffer& right) { // 定制交换 } } app::Buffer a; app::Buffer b; using std::swap; swap(a, b); // ADL 找到 app::swap |
Hidden friend- 可以把定制函数定义成类内友元
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace app { class Login { std::string username_; public: friend void encode_value( Encoder& encoder, const Login& value) { // ... } }; } |
|
1 2 3 4 5 6 7 |
// 这个函数通常只能通过 ADL 自然找到,所以称为 hidden friend // 优点: 不污染普通名字查找; 定制操作和类型放在一起; 可以访问私有成员; 不会轻易成为无关调用的候选 |
ADL的陷阱- 过于宽泛的函数模板可能意外加入候选集合
- 引入第三方类型后,关联命名空间发生变化
- 同名函数容易产生重载歧义
- 限定调用会关闭
ADL - 不限定调用又可能发生递归
- 很难从调用点看出最终函数来自哪里
CPO:统一的定制点对象
Customization Point Object,定制点对象- 它通常是一个
inline constexpr函数对象
- 它通常是一个
|
1 2 3 4 5 6 7 |
struct encode_fn { template<class T> void operator()(Encoder&, const T&) const; }; inline constexpr encode_fn encode{}; |
|
1 2 3 4 5 |
// 调用形式仍然像函数 api::encode(encoder, message); // 但实际上调用 api::encode.operator()(encoder, message); |
CPO可以在内部明确规定:- 优先调用成员函数;
- 否则尝试
ADL - 否则采用默认实现
- 都不满足则让约束失败
一个完整的 CPO 示例
- 下面的
CPO支持两种定制方式:message.encode(encoder)ADL找到encode_value(encoder, message)
- 成员函数优先
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
#include <concepts> #include <iostream> #include <string> #include <string_view> class Encoder { public: void write(std::string_view text) { std::cout << text << '\n'; } }; namespace api::detail { // 阻止普通名字查找找到无关的全局函数, // 同时允许 ADL 添加候选。 void encode_value() = delete; template<class T> concept MemberEncodable = requires( const T& value, Encoder& encoder ) { { value.encode(encoder) } -> std::same_as<void>; }; template<class T> concept AdlEncodable = requires( const T& value, Encoder& encoder ) { { encode_value(encoder, value) } -> std::same_as<void>; }; template<class T> void call_adl_encode( Encoder& encoder, const T& value) { encode_value(encoder, value); } } namespace api { struct encode_fn { template<class T> requires detail::MemberEncodable<T> void operator()( Encoder& encoder, const T& value) const { value.encode(encoder); } template<class T> requires ( !detail::MemberEncodable<T> && detail::AdlEncodable<T> ) void operator()( Encoder& encoder, const T& value) const { detail::call_adl_encode( encoder, value ); } }; inline constexpr encode_fn encode{}; } |
|
1 2 3 4 5 6 7 8 9 10 |
int main() { Encoder encoder; app::Login login{"Aet"}; app::Heartbeat heartbeat{1000}; api::encode(encoder, login); api::encode(encoder, heartbeat); } |
- 为什么
ADL函数不叫encode
|
1 2 3 4 5 6 7 8 9 |
// CPO 对象已经叫 api::encode // 如果内部又进行不受控制的 encode(encoder, value); // 很容易再次找到 CPO 自身,造成递归或名字冲突 // 示例中把底层 ADL 协议命名为 encode_value // 公共入口则是 api::encode |
CPO 比裸函数好在哪里
- 普通函数模板
|
1 2 3 |
template<class T> void encode(Encoder&, const T&); // 容易和用户自己的同名模板发生重载干扰 |
CPO对象- 自身不能像普通函数那样被随意添加重载
- 定制顺序由
operator()内部控制 - 可以使用
Concepts明确候选条件 - 可以作为对象传递给高阶算法
- 可以统一成员、
ADL和默认行为 - 公共调用形式稳定
|
1 |
inline constexpr encode_fn encode{}; |
tag_invoke 是什么
- 一些库使用统一的底层
ADL函数
|
1 |
tag_invoke(tag, arguments...) |
|
1 2 |
// 每个 CPO 本身作为第一个 tag 参数 tag_invoke(encode_tag, encoder, message); |
- 概念形式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct encode_t { template<class T> auto operator()( Encoder& encoder, const T& value) const { return tag_invoke( *this, encoder, value ); } }; |
|
1 2 3 4 5 6 |
// 用户通过隐藏友元定制 friend void tag_invoke( encode_t, Encoder&, const Login&); |
怎样选择定制机制
| 需求 | 推荐机制 |
| 类型映射到命令号或关联类型 | traits |
| 组件选择错误、锁或分配策略 | policy |
| 调用方明确选择阻塞/非阻塞 | tag 参数 |
| 基类调用派生类静态实现 | CRTP |
| 操作自然属于用户类型命名空间 | ADL/hidden friend |
| 公共库需要统一稳定入口 | CPO |
| 检查定制实现是否合法 | Concepts |
| 类型集合运行时变化 | 虚函数或类型擦除 |
网络组件中的推荐分层
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template<class Message> struct message_traits; // 命令号、版本等静态信息 template<class ErrorPolicy> class Decoder; // 错误处理策略 struct network_order_t; // 编码方式 tag struct host_order_t; api::encode(encoder, message); // 公共 CPO encode_value(encoder, message); // 用户 ADL 定制 |
常见错误
- 随意特化
std中的模板- 只有标准明确允许时才能添加特化。通常应定义自己的
traits
- 只有标准明确允许时才能添加特化。通常应定义自己的
- 把运行时状态放进静态
traits- 个连接的重试次数不同,就应该存放在策略对象或连接对象中,而不是
|
1 |
static constexpr int retry_count; |
Policy组合过多- 会造成复杂类型、代码膨胀和编译依赖。并非所有变化维度都应该模板化
|
1 2 3 4 5 6 7 8 |
Component< AllocatorPolicy, LockPolicy, ErrorPolicy, RetryPolicy, LogPolicy, CachePolicy > |
- 把
CRTP当运行时多态CRTP本身不能提供统一的非模板运行时基类接口
|
1 |
std::vector<MessageHandlerBase*> handlers; |
- 限定调用意外关闭
ADL
|
1 |
std::swap(a, b); |
CPO与ADL使用相同名字导致递归- 公共对象与内部 ADL 协议最好明确隔离
Concept检查通过就认为运行时安全Concept无法证明引用生命周期、线程安全和协议数据合法性
模板实例化与工程构建
从模板到具体函数
|
1 2 3 4 5 |
template<class T> T add(T left, T right) { return left + right; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int a = add(1, 2); double b = add(1.5, 2.5); // 隐式实例化 int add<int>(int left, int right) { return left + right; } double add<double>( double left, double right) { return left + right; } |
模板不会一次性实例化所有内容
- 考虑类模板
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<class T> class Box { public: void print() { std::cout << value_ << '\n'; } void call_missing_function() { value_.missing_function(); } private: T value_{}; }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
Box<int> box; box.print(); // 虽然 int 没有 missing_function() // 程序仍然可以编译,因为 Box<int>::call_missing_function() // 没有被使用,通常不需要实例化其函数体。 // 一旦调用 box.call_missing_function(); // 编译器才会把函数体中的:value_.missing_function() 代入成对 int 的操作,并产生错误 |
- 因此
- 类模板被使用,不等于所有成员函数体都立即实例化
为什么模板通常放在头文件
- 普通函数可以这样分离
|
1 2 |
// add.hpp int add(int, int); |
|
1 2 3 4 5 |
// add.cpp int add(int left, int right) { return left + right; } |
- 调用者只需要知道函数签名,因为
add.cpp会直接生成唯一的add(int,int) - 模板不同
|
1 2 3 |
// add.hpp template<class T> T add(T, T); |
|
1 2 3 4 5 6 |
// 当 main.cpp 调用 add(1, 2); // 编译 main.cpp 时需要生成 add<int> 要生成它,编译器必须看到模板函数体 |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 果定义只在另一个 .cpp 中: // add.cpp template<class T> T add(T left, T right) { return left + right; } // main.cpp 看不到定义,不能正常实例化 add<int>。最后通常出现 // undefined reference to `int add<int>(int, int)' |
- 所以最常见写法是把定义放在头文件:
|
1 2 3 4 5 6 7 8 |
// add.hpp #pragma once template<class T> T add(T left, T right) { return left + right; } |
.tpp 文件不是真正的单独编译
- 有些项目这样组织:
|
1 2 3 4 5 6 7 |
// vector.hpp template<class T> class Vector { public: void push_back(const T&); }; |
|
1 2 3 4 5 6 |
// vector.tpp template<class T> void Vector<T>::push_back(const T& value) { // ... } |
- 然后在头文件末尾:
- 这只是把模板实现从视觉上分离
- 预处理之后,
.tpp内容仍然出现在每一个包含vector.hpp的翻译单元中,并不是像普通.cpp一样独立编译
|
1 |
#include "vector.tpp" |
|
1 2 3 4 5 6 7 |
// 常见后缀: // 都只是项目约定,C++ 标准没有为它们规定特殊行为 .tpp .ipp .inl .hxx |
多个 .cpp 都实例化同一个模板怎么办
- 假设:
|
1 2 3 4 5 6 |
// util.hpp template<class T> T square(T value) { return value * value; } |
|
1 2 3 4 5 6 7 8 |
// a.cpp: #include "util.hpp" int a() { return square(3); } |
|
1 2 3 4 5 6 7 8 |
// b.cpp #include "util.hpp" int b() { return square(4); } |
|
1 2 |
// 两个翻译单元都可能生成 square<int> |
- 典型编译器会把这类实体放入
COMDAT、weak symbol或类似链接器机制中,最终保留一个等价定义 - 具体机制不由
C++标准规定,但ODR允许满足条件的模板定义出现在多个翻译单元中 - 因此
- 头文件中定义模板
≠必然产生multiple definition
- 头文件中定义模板
ODR:单一定义规则
ODR是One Definition Rule- 某些实体在整个程序中只能有一个定义
- 模板、类定义和
inline实体可以在多个翻译单元出现 - 这些多份定义必须满足严格的一致性要求
- 模板最危险的
ODR问题之一是宏导致定义不同
|
1 2 3 4 5 6 7 8 9 |
template<class T> int mode() { #ifdef FAST_MODE return 1; #else return 2; #endif } |
|
1 2 3 4 5 6 7 |
// 如果 a.cpp 定义了 FAST_MODE b.cpp 没有定义 FAST_MODE // 两个翻译单元会得到不同的 mode<int> // 链接器可能只选择其中一个,也可能没有给出任何错误 |
隐式实例化
- 正常调用触发的就是隐式实例化
|
1 2 3 4 5 6 7 |
template<class T> T maximum(T left, T right) { return left < right ? right : left; } int result = maximum(1, 2); |
|
1 2 3 4 5 6 7 8 9 10 |
// 编译器发现需要maximum<int> // 并且定义可见,于是隐式生成 //类模板也可能因不同需求发生不同程度的实例化 Box<int>* pointer; // 可能只需要知道它是一种类型 Box<int> object; // 需要知道完整对象布局 object.print(); // 需要实例化 print() 的函数体 |
显式实例化定义
- 可以明确要求编译器生成某个特化
|
1 2 3 4 5 6 7 8 |
template<class T> T maximum(T left, T right) { return left < right ? right : left; } // 显式实例化定义 template int maximum<int>(int, int); |
|
1 2 3 4 5 6 7 8 9 |
template<class T> class Parser { public: void parse(); }; // 显式实例化定义 template class Parser<Login>; |
- 它的含义不是“为
Login编写特殊算法”,而是- 使用通用模板定义,明确生成
Parser<Login>的相关实体
- 使用通用模板定义,明确生成
- 类模板的显式实例化定义还会涉及当时定义可见、符合规则且尚未特化的非模板成员。若只需要某个成员,也可以单独实例化
|
1 |
template void Parser<Login>::parse(); |
显式实例化声明:extern template
- 头文件
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// parser.hpp #pragma once template<class Message> class Parser { public: void parse(const Message& message) { // 大量模板代码 } }; extern template class Parser<Login>; extern template class Parser<Logout>; |
- 实现文件
|
1 2 3 4 5 |
// parser.cpp #include "parser.hpp" template class Parser<Login>; template class Parser<Logout>; |
- 其他翻译单元
|
1 2 3 4 5 6 7 8 |
// connection.cpp #include "parser.hpp" void process(const Login& login) { Parser<Login> parser; parser.parse(login); } |
extern template表示- 当前翻译单元不要为这个模板实参组合进行通常的隐式实例化
- 程序中的其他位置会提供显式实例化定义
|
1 2 3 4 5 |
// 声明:这里不要生成 extern template class Parser<Login>; // 定义:在这里集中生成 template class Parser<Login>; |
- 果只有
extern template,却忘记提供显式实例化定义,通常会出现链接错误
extern template 不保证解决所有编译时间问题
extern template可能减少- 多个翻译单元重复生成机器代码
- 优化器对同一特化进行重复优化
- 对象文件中的重复模板实体
- 但它不一定消除:
- 头文件预处理
- 模板定义解析
- 名字查找
- 约束检查
- 为类型检查而进行的部分实例化
- 内联相关分析
- 调用点的模板实参推导
显式实例化与显式特化的区别
- 显式实例化
- 算法没有改变
|
1 |
template class Parser<Login>; |
|
1 2 |
使用通用 Parser<T> 的实现 为 T = Login 生成代码 |
- 显式特化
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<class T> struct Serializer { static void encode(const T&) { // 通用实现 } }; template<> struct Serializer<LegacyMessage> { static void encode( const LegacyMessage&) { // LegacyMessage 专用实现 } }; |
|
1 2 |
LegacyMessage 不使用通用实现 而使用完全不同的模板定义 |
函数模板显式特化的头文件问题
- 函数模板
|
1 2 3 4 |
template<class T> void process(const T&) { } |
- 如果在头文件定义显式特化
- 这个显式特化本身不再是普通意义上的函数模板
- 多个翻译单元包含它,可能出现多重定义
|
1 2 3 4 |
template<> void process<Login>(const Login&) { } |
- 如果必须定义在头文件,应考虑
|
1 2 3 4 5 6 7 8 9 10 |
template<> inline void process<Login>( const Login&) { } // 或者只在头文件声明 template<> void process<Login>(const Login&); |
|
1 2 3 4 5 6 7 |
// 在一个 .cpp 中定义 template<> void process<Login>( const Login&) { } |
模板静态变量
C++17的inline static允许在类定义中直接定义
|
1 2 3 4 5 |
template<class T> struct Counter { inline static std::size_t value = 0; }; |
- 每个模板特化拥有自己的静态变量
- 它们是两个不同实体
|
1 2 |
Counter<int>::value Counter<double>::value |
- 同样,函数模板的局部静态变量也是每个特化一份
|
1 2 3 4 5 6 7 8 9 |
template<class T> std::size_t& counter() { static std::size_t value = 0; return value; } counter<int>() // int 特化的静态对象 counter<double>() // double 特化的静态对象 |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++20_第二篇03/21
- ♥ Effective C++_第三篇07/01
- ♥ WTL 概述03/10
- ♥ Windbg:远程调试,dump捕获06/04
- ♥ 51CTO:Linux C++网络编程三08/16
- ♥ C++_volatile10/08
