C++模板元编程实现选择排序

前言


模板元编程(template metaprogramming, TMP)是在编译期用类型和常量完成计算的技术。它的"变量"是类型与static constexpr值,"函数"是类模板特化,"循环"是递归实例化,"条件分支"是偏特化或if constexpr。用 TMP 实现选择排序,是一个能同时练到类型列表、元函数、递归终止和编译期断言的综合题目。


先说明两点预期。第一,TMP 版选择排序不会产生任何运行期排序代码,它产出的是一个新类型:元素按升序排列的类型列表。要把它变成运行期数据,还需要额外展开。第二,编译期计算有成本:每层递归都会实例化新模板,n个元素的选择排序大约需要 O(n²) 次实例化,递归深度为 O(n)。编译器对模板递归深度有默认上限(数百到一千量级,可用编译选项调整),元素过多会直接报"template instantiation depth exceeded"。


本文以 C++17 为基准,先搭类型列表和基础元函数,再实现min_index、remove_at和递归排序,最后给出 C++17constexpr函数的等价写法并比较两者适用场景。所有代码按标准语义推演,未经编译运行。


一、类型列表与编译期常量


TMP 里表示"一组类型"的常见载体是类型列表:


// 适用于 C++17
#include <cstddef>
#include <type_traits>

template <typename... Ts>
struct type_list {};

type_list没有任何成员,它只承载类型信息。编译期整数用std::integral_constant表示,std::integral_constant<int, 5>::value是常量 5,::type是它自己。为了少写几个字,可以起一个别名:


// 适用于 C++17
template <int V>
using ic = std::integral_constant<int, V>;

这样ic<5>就是一个"值为 5 的类型"。排序的对象就是type_list<ic<5>, ic<2>, ...>。


元函数通常写成类模板,用嵌套的type输出结果类型,用static constexpr成员输出值。调用时写typename foo<...>::type,其中的typename不能省——编译器默认不知道依赖模板参数的::type是类型还是静态成员,typename是在消除这个歧义。


二、四个基础元函数


求长度:用sizeof...直接数参数包。


// 适用于 C++17
template <typename List>
struct list_size;

template <typename... Ts>
struct list_size<type_list<Ts...>>
: std::integral_constant<std::size_t, sizeof...(Ts)> {};

按下标取类型:递归剥掉前面的元素。主模板只声明,两个偏特化分别处理"下标为 0"和"下标大于 0"。


// 适用于 C++17
template <std::size_t I, typename List>
struct list_at;

template <typename T, typename... Ts>
struct list_at<0, type_list<T, Ts...>> {
using type = T;
};

template <std::size_t I, typename T, typename... Ts>
struct list_at<I, type_list<T, Ts...>>
: list_at<I - 1, type_list<Ts...>> {};

拼接:把两个列表的包合并到一个新的type_list里。


// 适用于 C++17
template <typename A, typename B>
struct list_concat;

template <typename... As, typename... Bs>
struct list_concat<type_list<As...>, type_list<Bs...>> {
using type = type_list<As..., Bs...>;
};

删除指定下标:用一个累加器保存已扫描过的前缀,递归到目标位置时把该元素丢掉,再与剩余部分拼接。


// 适用于 C++17
template <std::size_t I, typename Acc, typename Rest>
struct remove_at_impl;

template <typename Acc, typename T, typename... Ts>
struct remove_at_impl<0, Acc, type_list<T, Ts...>> {
using type = typename list_concat<Acc, type_list<Ts...>>::type;
};

template <std::size_t I, typename Acc, typename T, typename... Ts>
struct remove_at_impl<I, Acc, type_list<T, Ts...>>
: remove_at_impl<I - 1,
typename list_concat<Acc, type_list<T>>::type,
type_list<Ts...>> {};

template <std::size_t I, typename List>
struct remove_at {
using type = typename remove_at_impl<I, type_list<>, List>::type;
};

remove_at_impl的第二个模板参数是"已经处理过的前缀"。每递归一层,前缀多一个元素,待处理列表少一个元素,I减一。当I归零时,当前列表的首元素就是要删除的那个,把前缀与Ts...拼接即可。


三、选择排序的递归实现


选择排序的步骤是:在剩余元素中找到最小值的下标,把它放到结果开头,对剩余部分递归。编译期版本需要两个比较器相关的元函数:less和min_index。


// 适用于 C++17
template <typename A, typename B>
struct value_less
: std::bool_constant<(A::value < B::value)> {};

template <typename List, std::size_t I, std::size_t Best,
template <typename, typename> class Less,
bool Done = (I >= list_size<List>::value)>
struct min_index_impl;

template <typename List, std::size_t I, std::size_t Best,
template <typename, typename> class Less>
struct min_index_impl<List, I, Best, Less, true> {
static constexpr std::size_t value = Best;
};

template <typename List, std::size_t I, std::size_t Best,
template <typename, typename> class Less>
struct min_index_impl<List, I, Best, Less, false> {
using current = typename list_at<I, List>::type;
using best = typename list_at<Best, List>::type;
static constexpr std::size_t next =
Less<current, best>::value ? I : Best;
static constexpr std::size_t value =
min_index_impl<List, I + 1, next, Less,
(I + 1 >= list_size<List>::value)>::value;
};

template <typename List, template <typename, typename> class Less>
struct min_index {
static constexpr std::size_t value =
min_index_impl<List, 1, 0, Less,
(list_size<List>::value <= 1)>::value;
};

这里用了一个bool Done参数作为递归终止开关。当I达到列表长度时,Done为真,命中第一个偏特化返回当前最优下标;否则命中第二个,比较current与best后继续。把终止条件做成模板参数的默认值,可以避免为"空列表"和"非空列表"分别写一套接口。


有了min_index,排序本身只有几行:


// 适用于 C++17
template <typename List, template <typename, typename> class Less,
bool Done = (list_size<List>::value <= 1)>
struct selection_sort_impl;

template <typename List, template <typename, typename> class Less>
struct selection_sort_impl<List, Less, true> {
using type = List; // 零个或一个元素,天然有序
};

template <typename List, template <typename, typename> class Less>
struct selection_sort_impl<List, Less, false> {
static constexpr std::size_t min_i = min_index<List, Less>::value;
using head = typename list_at<min_i, List>::type;
using rest = typename remove_at<min_i, List>::type;
using type = typename list_concat<
type_list<head>,
typename selection_sort_impl<rest, Less>::type>::type;
};

template <typename List, template <typename, typename> class Less>
using selection_sort_t = typename selection_sort_impl<List, Less>::type;

selection_sort_impl的第三个参数默认按列表长度计算,所以递归调用selection_sort_impl<rest, Less>时不必手动传终止标志。


最后的静态断言把结果"钉"在类型上:


// 适用于 C++17
using input = type_list<ic<5>, ic<2>, ic<9>, ic<1>, ic<5>, ic<3>>;
using sorted = selection_sort_t<input, value_less>;

static_assert(std::is_same_v<
sorted,
type_list<ic<1>, ic<2>, ic<3>, ic<5>, ic<5>, ic<9>>>, "sort failed");

static_assert在编译期检查类型是否完全一致。若排序逻辑写错,编译就会失败,而不是等到运行。


四、C++17 的 constexpr 替代方案


如果排序的只是整数,而不是"类型",没有必要动用 TMP。C++14 起constexpr函数支持循环和局部变量,C++17 起std::array的非 const 下标也可在常量表达式中使用,因此可以直接写普通的循环版选择排序:


// 适用于 C++17
#include <array>
#include <cstddef>

template <std::size_t N>
constexpr std::array<int, N> sort_values(std::array<int, N> a) {
for (std::size_t i = 0; i + 1 < N; ++i) {
std::size_t m = i;
for (std::size_t j = i + 1; j < N; ++j) {
if (a[j] < a[m]) {
m = j;
}
}
int tmp = a[i];
a[i] = a[m];
a[m] = tmp;
}
return a;
}

constexpr auto result = sort_values(std::array<int, 5>{5, 2, 9, 1, 3});
static_assert(result[0] == 1 && result[4] == 9, "sort failed");

这段代码更短、更易读,编译期开销通常也更小。它和 TMP 版的区别在于能处理的对象:constexpr函数处理的是值,结果是一个常量数组;TMP 处理的是类型,结果是一个类型列表,可以继续参与类型计算(例如按类型选择重载、生成结构体字段)。选哪种取决于你要的是值还是类型。




维度TMP 类型列表constexpr 函数



处理对象类型 / 编译期常量值

结果新类型常量对象

可读性较低接近普通代码

编译开销O(n²) 次实例化通常更低

适用场景类型级计算、静态分派常量表、编译期数据



代价与适用边界


TMP 的编译期计算不是"零成本"的。每个递归层都会产生新的模板实例,编译器要为它们做名字查找、替换和记录;n个元素的选择排序需要约 n²/2 次min_index内部的list_at求值,而每次list_at本身又是 O(n) 的递归。整体实例化数量随 n 增长很快。


递归深度同样需要注意。min_index_impl和selection_sort_impl都是线性递归,深度与元素个数同阶。超过编译器的默认上限会报错,可以通过命令行选项调整,但把上限调得很大往往意味着编译时间不可接受。


因此 TMP 适合元素数量小、且确实需要类型级结果的场景,例如固定字段的序列化描述、编译期注册表、维度检查。纯粹为了排序整数而用 TMP,属于技术练习,工程上应优先选constexpr函数或运行期std::sort。


常见坑点



  1. ❌ 用运行期循环的思路写元函数,想在一个模板里"遍历"参数包。✅ 元函数靠递归实例化与偏特化推进,每层处理一个元素。



  1. ❌ 在依赖名typename X<T>::type前漏写typename。✅ 编译器默认按值解释,必须显式标注这是类型。



  1. ❌ 对空列表调用list_at<0, List>。✅ 先判断list_size,或在排序入口处理零/一个元素的终止情况。



  1. ❌ 给函数模板写偏特化来实现分支。✅ 函数模板不支持偏特化;把逻辑放进类模板,或改用if constexpr。



  1. ❌ 递归元函数只写递推分支,没有终止特化。✅ 必须有一个Done == true或空包的特化兜底,否则实例化无法停止。



  1. ❌ 认为static_assert通过就意味着生成了运行期排序代码。✅ 编译期只完成类型计算,运行期没有任何排序动作;需要数据时还得自己展开。



  1. ❌ 比较器在相等时返回true,破坏严格弱序。✅ 编译期less也必须是严格弱序,相等必须返回false,否则排序逻辑不成立。



  1. ❌ 对几十个元素强行用 TMP 排序,编译时间失控。✅ 控制在少量元素,或改用constexpr函数、运行期算法。


总结




元函数作用实例化开销



list_size取元素个数O(1)

list_at按下标取类型O(I)

list_concat拼接两个列表O(1) 次模板展开

remove_at删除指定下标O(n)

min_index找最小元素下标O(n) 次list_at

selection_sort_impl递归选择排序O(n²) 量级



TMP 版选择排序的价值在于把"递归、特化、类型计算"这套机制完整走一遍:类型列表是数据结构,偏特化是分支,递归实例化是循环,static_assert是测试。真正写工程代码时,能用constexpr就用constexpr,只有在结果必须是类型时才请出模板元编程。




相关新闻

100行代码手写Hermes式智能体框架并接入飞书机器人

100行代码手写Hermes式智能体框架并接入飞书机器人

1. 为什么我要自己动手写一个智能体框架1.1 从“玩具”到“工具”的转折点市面上开源的智能体框架我几乎都翻过一遍。LangChain 生态庞大&#xff0c;但依赖链太长&#xff0c;一个简单的对话循环要引入几十个包&#xff1b;AutoGen 的多智能体协作很惊艳&#xff0c;可一旦要接…

2026/10/20 6:48:42 阅读更多 →
技能管理系统Skills:用数据化技术画像精准定位自身能力短板

技能管理系统Skills:用数据化技术画像精准定位自身能力短板

从面试被问到“你还会什么”那一刻&#xff0c;我才意识到自己的技能清单有多失真。简历上写着“熟悉微服务”&#xff0c;可真聊起服务拆分和容错设计&#xff0c;脑子里能拿出来的只有几个名词&#xff1b;写了三年业务代码&#xff0c;却说不清自己到底擅长什么、短板在哪里…

2026/10/20 6:48:42 阅读更多 →
国产大模型崛起:榜单前20占6席,技术底座与落地实践全解析

国产大模型崛起:榜单前20占6席,技术底座与落地实践全解析

1. 榜单背后的真实格局&#xff1a;国产大模型凭什么挤进前二十全球AI模型榜单前20名里国产模型占了6个&#xff0c;这个数字放在三年前几乎没人敢想。我翻了一圈榜单的评测维度&#xff0c;发现这次上榜的国产模型覆盖了文本理解、代码生成、多模态推理、长上下文处理几个主要…

2026/10/20 6:47:42 阅读更多 →

最新新闻

Abaqus隧道空洞模拟:地表沉降有限元分析建模方法与工程应用

Abaqus隧道空洞模拟:地表沉降有限元分析建模方法与工程应用

从实际项目出发&#xff0c;我接到过一个很有意思的模拟任务&#xff1a;用Abaqus分析隧道施工中形成的空洞对地表沉降的影响规律。这类问题在岩土工程里特别典型——城市地下管线破损、盾构注浆不密实、矿山采空区塌落&#xff0c;都会在地层内部留下空洞&#xff0c;而地表沉…

2026/10/20 7:38:07 阅读更多 →
轻量级Coding Agent设计:如何通过架构减法节省58.7% Token

轻量级Coding Agent设计:如何通过架构减法节省58.7% Token

1. 项目概述&#xff1a;一个轻量级 Coding Agent 的诞生逻辑“我做了一个 Coding Agent&#xff0c;结果比 Claude Code 少用了 58.7% Token”——这句话刚在技术社区刷屏时&#xff0c;我第一反应不是惊讶&#xff0c;而是立刻点开原文代码仓库&#xff0c;翻看它的 prompt 结…

2026/10/20 7:38:07 阅读更多 →
用APIKit与Param Miner挖掘隐藏API端点:Burp Suite主动探测实战指南

用APIKit与Param Miner挖掘隐藏API端点:Burp Suite主动探测实战指南

先问个扎心的问题&#xff1a;做接口测试或者安全评估的时候&#xff0c;你是不是还停留在“开Burp、挂代理、点开HTTP History看请求”这个阶段&#xff1f;如果是&#xff0c;那我要说一句可能不太中听的话——你手里的Burp Suite&#xff0c;至少有一半的潜力被你浪费了。经…

2026/10/20 7:38:07 阅读更多 →
MIL-C-5015军规连接器振动加速度传感器:选型安装与排查

MIL-C-5015军规连接器振动加速度传感器:选型安装与排查

干设备状态监测好些年&#xff0c;有一类传感器是小众但特别关键的——带军规连接器的振动加速度传感器。光看名字“MIL-C-5015振动加速度传感器5/8-24UNF军标连接器”就够劝退了&#xff0c;但如果你在航空发动机试车台、燃气轮机厂房、重型机械现场转过一圈&#xff0c;一定会…

2026/10/20 7:38:07 阅读更多 →
杰理平台time PWM实战:定时器比较输出的配置与踩坑指南

杰理平台time PWM实战:定时器比较输出的配置与踩坑指南

前阵子帮朋友调一块杰理平台的小板子&#xff0c;现象很蹊跷&#xff1a;按键控制LED亮度时&#xff0c;IO口确实有电平变化&#xff0c;但用示波器一看&#xff0c;波形完全不是PWM该有的样子&#xff0c;反而是一串不规则的脉冲&#xff0c;频率还飘。翻遍SDK里的例程&#x…

2026/10/20 7:38:07 阅读更多 →
纯.NET自绘打印模板设计器:拖拽式可视化与动态渲染

纯.NET自绘打印模板设计器:拖拽式可视化与动态渲染

简介&#xff1a;这是一套面向.NET开发者的C#可视化打印模板设计源码&#xff0c;纯.NET实现、不依赖任何第三方控件&#xff0c;适合需要为发票、报告、证书、标签及单据类业务快速搭建打印能力的初中级工程师。资源提供模板编辑器、图形设计工具与布局管理器等组件&#xff0…

2026/10/20 7:37:06 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →