
什么是 tag_dispatch,它解决什么问题? C++ 没有“编译期函数重载选择”这个语法糖,但你经常需要根据类型特征(比如是否是 std::random_access_iterator、是否支持 noexcept 移动)走不同实现路径。硬写 if constexpr 或模板特化太散,enable_if 又容易让函数签名爆炸。标签分派就是用一个轻量、无状态的空类型(即“标签”)把重载决策推给编译器——靠函数参数类型匹配,而不是条件编译。
它的核心不是新语法,而是**用重载 + 空结构体作为类型标记**,让编译器在多个同名函数中自动选最匹配的那个。
怎么写一个最小可用的 tag_dispatch 示例? 关键就三步:定义标签、写重载函数、用 decltype 或 std::iterator_traits 推导出对应标签传进去。
常见错误:把标签写成带数据成员的 Struct,或忘了给重载函数加 const& 参数修饰,导致匹配失败。
- 标签必须是空类(
struct random_access_tag {};),不能有构造函数、成员变量或虚函数 - 重载函数参数列表里,标签类型必须是**唯一区分点**,其他参数保持一致(比如都接受
Iterator) - 调用时别手写标签类型,用
iterator_category或自定义 trait 提取,例如:foo(it, typename std::iterator_traits<it>::iterator_category{})</it>
struct input_tag {}; struct random_access_tag {}; template<typename It> void advance_impl(It& it, int n, input_tag) { while (n--) ++it; } template<typename It> void advance_impl(It& it, int n, random_access_tag) { it += n; // 直接算术运算 } template<typename It> void advance(It& it, int n) { advance_impl(it, n, typename std::iterator_traits<It>::iterator_category{}); }
为什么不用 if constexpr 或 enable_if?
不是不能用,而是场景不同:if constexpr 适合逻辑分支少、共用大部分代码的场合;enable_if 容易让函数模板变成“一长串约束条件”,可读性差,且 SFINAE 错误信息难懂。
- 标签分派天然支持**多级分类**(比如
input_tag→forward_tag→bidirectional_tag→random_access_tag),继承关系还能复用父类实现 - 所有重载函数都在同一作用域,ide 更容易跳转、补全
- 编译器报错时,会明确说“找不到匹配
advance_impl(It&, int, forward_tag)的重载”,比 “no type named ‘type’ in …” 清晰得多
实际项目里容易踩的坑
标签分派看着简单,但真正在容器、算法或 traits 封装里用起来,最容易栽在“标签推导不一致”和“隐式转换干扰匹配”上。
立即学习“C++免费学习笔记(深入)”;
- 别在标签之间写用户定义转换(比如
operator random_access_tag()),这会让重载解析失效或选错函数 - 如果你自己定义 trait(比如
is_contiguous_v),返回的标签类型必须和调用处期望的一致,否则编译器不会自动转型——空 struct 之间没有隐式关系 - 当模板参数本身是引用类型(如
T&),std::iterator_traits<t></t>可能不成立,得先std::remove_reference_t再查 trait - 调试时想看选了哪个重载?加个
static_assert(false, "this overload was chosen")在函数体开头,编译失败信息里就暴露了
标签分派真正的复杂点不在写法,而在设计时要提前想清楚分类层级是否正交、是否覆盖所有可能类型、以及未来加新标签会不会破坏现有继承链。一旦定下来,改起来比重构一堆 if constexpr 还麻烦。