现代C++ :constexpr:一个常态的世界
引言我们已经连续讲了几讲比较累人的编译期编程了。今天我们还是继续这个话题但是相信今天学完之后你会感觉比之前几讲要轻松很多。C 语言里的很多改进让我们做编译期编程也变得越来越简单了。初识 constexpr我们先来看一些例子int sqr(int n) { return n * n; } int main() { int a[sqr(3)]; }想一想这个代码合法吗看过之后再想想这个代码如何int sqr(int n) { return n * n; } int main() { const int n sqr(3); int a[n]; }还有这个#include array int sqr(int n) { return n * n; } int main() { std::arrayint, sqr(3) a; }此外我们前面模板元编程里的那些类里的 static const int 什么的你认为它们能用在上面的几种情况下吗如果以上问题你都知道正确的答案那恭喜你你对 C 的理解已经到了一个不错的层次了。但问题依然在那里这些问题的答案不直观。并且我们需要一个比模板元编程更方便的进行编译期计算的方法。在 C11 引入、在 C14 得到大幅改进的 constexpr 关键字就是为了解决这些问题而诞生的。它的字面意思是 constant expression常量表达式。存在两类 constexpr 对象constexpr 变量唉……constexpr 函数一个 constexpr 变量是一个编译时完全确定的常数。一个 constexpr 函数至少对于某一组实参可以在编译期间产生一个编译期常数。注意一个 constexpr 函数不保证在所有情况下都会产生一个编译期常数因而也是可以作为普通函数来使用的。编译器也没法通用地检查这点。编译器唯一强制的是constexpr 变量必须立即初始化初始化只能使用字面量或常量表达式后者不允许调用任何非 constexpr 函数constexpr 的实际规则当然稍微更复杂些而且随着 C 标准的演进也有着一些变化特别是对 constexpr 函数如何实现的要求在慢慢放宽。下面我们也会回到这个问题略作展开。拿 constexpr 来改造开头的例子下面的代码就完全可以工作了#include array constexpr int sqr(int n) { return n * n; } int main() { constexpr int n sqr(3); std::arrayint, n a; int b[n]; }要检验一个 constexpr 函数能不能产生一个真正的编译期常量可以把结果赋给一个 constexpr 变量。成功的话我们就确认了至少在这种调用情况下我们能真正得到一个编译期常量。constexpr 和编译期计算上面这些当然有点用。但如果只有这点用的话就不值得我专门来写一讲了。更强大的地方在于使用编译期常量就跟我们之前的那些类模板里的 static const int 变量一样是可以进行编译期计算的。constexpr int factorial(int n) { if (n 0) { return 1; } else { return n * factorial(n - 1); } }然后我们用下面的代码可以验证我们确实得到了一个编译期常量int main() { constexpr int n factorial(10); printf(%d\n, n); }编译可以通过同时如果我们看产生的汇编代码的话一样可以直接看到常量 3628800。这里有一个问题在这个 constexpr 函数里是不能写 static_assert(n 0) 的。一个 constexpr 函数仍然可以作为普通函数使用——显然传入一个普通 int 是不能使用静态断言的。替换方法是在 factorial 的实现开头加入if (n 0) { throw std::invalid_argument( Arg must be non-negative); }如果你在 main 里写 constexpr int n factorial(-1); 的话就会看到编译器报告抛出异常导致无法得到一个常量表达式。建议你自己尝试一下。constexpr 和 const初学 constexpr 时一个很可能有的困惑是它跟 const 用法上的区别到底是什么。产生这种困惑是正常的毕竟 const 是个重载了很多不同含义的关键字。const 的原本和基础的含义自然是表示它修饰的内容不会变化如const int n 1: n 2; // 出错注意 const 在类型声明的不同位置会产生不同的结果。对于常见的 const char* 这样的类型声明意义和 char const* 相同是指向常字符的指针指针指向的内容不可更改但和 char * const 不同那代表指向字符的常指针指针本身不可更改。本质上const 用来表示一个运行时常量。在 C 里const 后面渐渐带上了现在的 constexpr 用法也代表编译期常数。现在——在有了 constexpr 之后——我们应该使用 constexpr 在这些用法中替换 const 了。从编译器的角度为了向后兼容性const 和 constexpr 在很多情况下还是等价的。但有时候它们也有些细微的区别其中之一为是否内联的问题。内联变量C17 引入了内联inline变量的概念允许在头文件中定义内联变量然后像内联函数一样只要所有的定义都相同那变量的定义出现多次也没有关系。对于类的静态数据成员const 缺省是不内联的而 constexpr 缺省就是内联的。这种区别在你用 去取一个 const int 值的地址、或将其传到一个形参类型为 const int 的函数去的时候这在 C 文档里的行话叫 ODR-use就会体现出来。下面是个合法的完整程序#include iostream struct magic { static const int number 42; }; int main() { std::cout magic::number std::endl; }我们稍微改一点#include iostream #include vector struct magic { static const int number 42; }; int main() { std::vectorint v; // 调用 push_back(const T) v.push_back(magic::number); std::cout v[0] std::endl; }程序在链接时就会报错了说找不到 magic::number注意MSVC 缺省不报错但使用标准模式——/Za 命令行选项——也会出现这个问题。这是因为 ODR-use 的类静态常量也需要有一个定义在没有内联变量之前需要在某一个源代码文件非头文件中这样写const int magic::number;必须正正好好一个多了少了都不行所以叫 one definition rule。内联函数现在又有了内联变量以及模板则不受这条规则限制。修正这个问题的简单方法是把 magic 里的 static const 改成 static constexpr 或 static inline const。前者可行的原因是类的静态 constexpr 成员变量默认就是内联的。const 常量和类外面的 constexpr 变量不默认内联需要手工加 inline 关键字才会变成内联。constexpr 变量模板变量模板是 C14 引入的新概念。之前我们需要用类静态数据成员来表达的东西使用变量模板可以更简洁地表达。constexpr 很合适用在变量模板里表达一个和某个类型相关的编译期常量。由此type traits 都获得了一种更简单的表示方式。再看一下我们用过的例子template class T inline constexpr bool is_trivially_destructible_v is_trivially_destructible T::value;了解了变量也可以是模板之后上面这个代码就很容易看懂了吧这只是一个小小的语法糖允许我们把 is_trivially_destructible::value 写成 is_trivially_destructible_v。constexpr 变量仍是 const一个 constexpr 变量仍然是 const 常类型。需要注意的是就像 const char* 类型是指向常量的指针、自身不是 const 常量一样下面这个表达式里的 const 也是不能缺少的constexpr int a 42; constexpr const int b a;第二行里constexpr 表示 b 是一个编译期常量const 表示这个引用是常量引用。去掉这个 const 的话编译器就会认为你是试图将一个普通引用绑定到一个常数上报一个类似下面的错误信息error: binding reference of type ‘int’ to ‘const int’ discards qualifiers如果按照 const 位置的规则constexpr const int b 实际该写成 const int constexpr b。不过constexpr 不需要像 const 一样有复杂的组合因此永远是写在类型前面的。constexpr 构造函数和字面类型一个合理的 constexpr 函数应当至少对于某一组编译期常量的输入能得到编译期常量的结果。为此对这个函数也是有些限制的最早constexpr 函数里连循环都不能有但在 C14 放开了。目前constexpr 函数仍不能有 try … catch 语句和 asm 声明但到 C20 会放开。constexpr 函数里不能使用 goto 语句。等等。一个有意思的情况是一个类的构造函数。如果一个类的构造函数里面只包含常量表达式、满足对 constexpr 函数的限制的话这也意味着里面不可以有任何动态内存分配并且类的析构函数是平凡的那这个类就可以被称为是一个字面类型。换一个角度想对 constexpr 函数——包括字面类型构造函数——的要求是得让编译器能在编译期进行计算而不会产生任何“副作用”比如内存分配、输入、输出等等。为了全面支持编译期计算C14 开始很多标准类的构造函数和成员函数已经被标为 constexpr以便在编译期使用。当然大部分的容器类因为用到了动态内存分配不能成为字面类型。下面这些不使用动态内存分配的字面类型则可以在常量表达式中使用下面这个玩具例子可以展示上面的若干类及其成员函数的行为#include array #include iostream #include memory #include string_view using namespace std; int main() { constexpr string_view sv{hi}; constexpr pair pr{sv[0], sv[1]}; constexpr array a{pr.first, pr.second}; constexpr int n1 a[0]; constexpr int n2 a[1]; cout n1 n2 \n; }编译器可以在编译期即决定 n1 和 n2 的数值从最后结果的角度上面程序就是输出了两个整数而已。if constexpr上一讲的结尾我们给出了一个在类型参数 C 没有 reserve 成员函数时不能编译的代码template typename C, typename T void append(C container, T* ptr, size_t size) { if (has_reserveC::value) { container.reserve( container.size() size); } for (size_t i 0; i size; i) { container.push_back(ptr[i]); } }在 C17 里我们只要在 if 后面加上 constexpr代码就能工作了 [2]。当然它要求括号里的条件是个编译期常量。满足这个条件后标签分发、enable_if 那些技巧就不那么有用了。显然使用 if constexpr 能比使用其他那些方式写出更可读的代码……output_container.h 解读到了今天我们终于把 output_container.h[3]用到的 C 语法特性都讲过了我们就拿里面的代码来讲解一下让你加深对这些特性的理解。// Type trait to detect std::pair template typename T struct is_pair : std::false_type {}; template typename T, typename U struct is_pairstd::pairT, U : std::true_type {}; template typename T inline constexpr bool is_pair_v is_pairT::value;这段代码利用模板特化和 false_type、true_type 类型定义了 is_pair用来检测一个类型是不是 pair。随后我们定义了内联 constexpr 变量本讲is_pair_v用来简化表达。// Type trait to detect whether an // output function already exists template typename T struct has_output_function { template class U static auto output(U* ptr) - decltype( std::declvalstd::ostream() *ptr, std::true_type()); template class U static std::false_type output(...); static constexpr bool value decltype( outputT(nullptr))::value; }; template typename T inline constexpr bool has_output_function_v has_output_functionT::value;这段代码使用 SFINAE 技巧来检测模板参数 T 的对象是否已经可以直接输出到 ostream。然后一样用一个内联 constexpr 变量来简化表达。// Output function for std::pair template typename T, typename U std::ostream operator( std::ostream os, const std::pairT, U pr);再然后我们声明了一个 pair 的输出函数标准库没有提供这个功能。我们这儿只是声明是因为我们这儿有两个输出函数且可能互相调用。所以我们要先声明其中之一。下面会看到pair 的通用输出形式是“(x, y)”。// Element output function for // containers that define a key_type // and have its value type as // std::pair template typename T, typename Cont auto output_element( std::ostream os, const T element, const Cont, const std::true_type) - decltype( std::declval typename Cont::key_type(), os); // Element output function for other // containers template typename T, typename Cont auto output_element( std::ostream os, const T element, const Cont, ...) - decltype(os);对于容器成员的输出我们也声明了两个不同的重载。我们的意图是如果元素的类型是 pair 并且容器定义了一个 key_type 类型我们就认为遇到了关联容器输出形式为“x y”而不是“(x, y)”。// Main output function, enabled // only if no output function // already exists template typename T, typename std::enable_if_t !has_output_function_vT auto operator(std::ostream os, const T container) - decltype(container.begin(), container.end(), os) …主输出函数的定义。注意这儿这个函数的启用有两个不同的 SFINAE 条件用 decltype 返回值的方式规定了被输出的类型必须有 begin() 和 end() 成员函数。用 enable_if_t 规定了只在被输出的类型没有输出函数时才启用这个输出函数。否则对于 string 这样的类型编译器发现有两个可用的输出函数就会导致编译出错。我们可以看到用 decltype 返回值的方式比较简单不需要定义额外的模板。但表达否定的条件还是要靠 enable_if。此外因为此处是需要避免有二义性的重载constexpr 条件语句帮不了什么忙。using element_type decay_tdecltype( *container.begin()); constexpr bool is_char_v is_same_velement_type, char; if constexpr (!is_char_v) { os { ; }对非字符类型我们在开始输出时先输出“{ ”。这儿使用了 decay_t是为了把类型里的引用和 const/volatile 修饰去掉只剩下值类型。如果容器里的成员是 char这儿会把 char 和 const char 还原成 char。后面的代码就比较简单了。可能唯一需要留意的是下面这句output_element( os, *it, container, is_pairelement_type());这儿我们使用了标签分发技巧来输出容器里的元素。要记得output_element 不纯粹使用标签分发还会检查容器是否有 key_type 成员类型。template typename T, typename Cont auto output_element( std::ostream os, const T element, const Cont, const std::true_type) - decltype( std::declval typename Cont::key_type(), os) { os element.first element.second; return os; } template typename T, typename Cont auto output_element( std::ostream os, const T element, const Cont, ...) - decltype(os) { os element; return os; }output_element 的两个重载的实现都非常简单应该不需要解释了。template typename T, typename U std::ostream operator( std::ostream os, const std::pairT, U pr) { os ( pr.first , pr.second ); return os; }同样pair 的输出的实现也非常简单。唯一需要留意的是上面三个函数的输出内容可能还是容器因此我们要将其实现放在后面确保它能看到我们的通用输出函数。要看一下用到 output_container 的例子。内容小结本讲我们介绍了编译期常量表达式和编译期条件语句可以看到这两种新特性对编译期编程有了很大的改进可以让代码变得更直观。最后我们讨论了我们之前用到的容器输出函数 output_container 的实现里面用到了多种我们目前讨论过的编译期编程技巧。

相关新闻

YOLO训练loss不收敛的排查清单:从学习率到数据增强的20个调试点

YOLO训练loss不收敛的排查清单:从学习率到数据增强的20个调试点

引言:为什么你的YOLO模型训不好? 很多开发者拿到YOLO后,直接复制一段model.train()代码就开始跑,结果发现——mAP卡在0.3上不去、Loss震荡不收敛、训练集表现很好但测试集一塌糊涂(过拟合)。 根本原因在于:你没有理解每个参数背后的物理意义。 2026年,YOLO家族迎来了…

2026/7/20 8:17:10阅读更多 →
YOLOv11自定义数据集训练的YAML配置文件逐行解读:每个参数背后的意义

YOLOv11自定义数据集训练的YAML配置文件逐行解读:每个参数背后的意义

前言:别让配置文件成为你训练路上的第一个坑 凌晨三点,盯着屏幕上的训练日志,Loss曲线死活不收敛。明明改了网络结构,训练时却完全不生效——最后发现是YAML文件里一个缩进错了,两个空格被换成了Tab键。 这是很多CV开发者第一次接触YOLOv11时都会踩的坑。 很多人把YAML…

2026/7/20 1:05:10阅读更多 →
【AI问数】自然语言查询 + RAG引擎:AI问数的黄金搭档

【AI问数】自然语言查询 + RAG引擎:AI问数的黄金搭档

70% 查询占比 95% RAG增强准确率 5 查询类型 <3秒 响应时间 自然语言查询模块RAG引擎是AI问数的黄金搭档&#xff0c;承担70%的用户请求。支持单表查询、多表JOIN、嵌套子查询、窗口函数、聚合分析。结合四维RAG&#xff0c;复杂查询准确率从30%提升至90%。 一、支持的…

2026/7/20 5:04:05阅读更多 →
深入解析TI处理器PRCM模块:从时钟门控到低功耗设计的实战指南

深入解析TI处理器PRCM模块:从时钟门控到低功耗设计的实战指南

1. 项目概述&#xff1a;从寄存器手册到实战的时钟管理如果你是一名嵌入式软件或底层驱动工程师&#xff0c;面对动辄上千页的处理器技术参考手册&#xff0c;尤其是关于电源、复位和时钟管理&#xff08;PRCM&#xff09;的章节&#xff0c;是否曾感到无从下手&#xff1f;手册…

2026/7/20 12:03:51阅读更多 →
applera1n终极指南:如何免费绕过iOS激活锁限制

applera1n终极指南:如何免费绕过iOS激活锁限制

applera1n终极指南&#xff1a;如何免费绕过iOS激活锁限制 【免费下载链接】applera1n icloud bypass for ios 15-16 项目地址: https://gitcode.com/gh_mirrors/ap/applera1n 当你面对一台无法激活的iPhone时&#xff0c;那种挫败感是难以言喻的。屏幕上的"此iPho…

2026/7/20 12:03:51阅读更多 →
5步快速上手:国家中小学智慧教育平台电子课本下载终极指南

5步快速上手:国家中小学智慧教育平台电子课本下载终极指南

5步快速上手&#xff1a;国家中小学智慧教育平台电子课本下载终极指南 【免费下载链接】tchMaterial-parser 国家中小学智慧教育平台 电子课本下载工具&#xff0c;帮助您从智慧教育平台中获取电子课本的 PDF 文件网址并进行下载&#xff0c;让您更方便地获取课本内容。 项目…

2026/7/20 12:03:51阅读更多 →
AM263P总线安全与MPU防火墙配置实战:构建高可靠嵌入式系统硬件隔离

AM263P总线安全与MPU防火墙配置实战:构建高可靠嵌入式系统硬件隔离

1. 项目概述&#xff1a;为什么我们需要在芯片内部“筑墙”&#xff1f;在嵌入式系统&#xff0c;尤其是汽车电子和工业控制这类对可靠性要求极高的领域&#xff0c;一个核心的挑战是如何确保系统内不同功能模块之间既能高效协作&#xff0c;又不会相互干扰。想象一下&#xff…

2026/7/20 12:03:51阅读更多 →
每天1小时,用AI量产YouTube视频:已验证的17步自动化流水线,含自动脚本→配音→字幕→SEO标题生成

每天1小时,用AI量产YouTube视频:已验证的17步自动化流水线,含自动脚本→配音→字幕→SEO标题生成

更多请点击&#xff1a; https://kaifayun.com 第一章&#xff1a;AI做YouTube视频 借助现代生成式AI工具&#xff0c;从脚本撰写、语音合成、画面生成到自动剪辑&#xff0c;全流程自动化制作YouTube视频已成为现实。关键在于构建可复用、模块化且可控的AI工作流&#xff0c;…

2026/7/20 12:03:51阅读更多 →
如何快速上手Transformers PHP:5分钟搭建你的第一个AI应用 [特殊字符]

如何快速上手Transformers PHP:5分钟搭建你的第一个AI应用 [特殊字符]

如何快速上手Transformers PHP&#xff1a;5分钟搭建你的第一个AI应用 &#x1f680; 【免费下载链接】transformers-php Transformers PHP is a toolkit for PHP developers to add machine learning magic to their projects easily. 项目地址: https://gitcode.com/gh_mi…

2026/7/20 12:01:50阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中&#xff0c;我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源&#xff0c;还是配置文件、证书等&#xff0c;都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下&#xff0c;但这…

2026/7/20 0:50:54阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP&#xff08;轻量级目录访问协议&#xff09;作为企业级身份认证的黄金标准&#xff0c;已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时&#xff0c;发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/20 0:50:54阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”&#xff0c;而是以可解释、可审计、可迭代的方式&#xff0c;赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/20 0:50:54阅读更多 →
2026 WAIC:努比亚二代“豆包手机”NaviX Ultra亮相,智能体验全面升级!

2026 WAIC:努比亚二代“豆包手机”NaviX Ultra亮相,智能体验全面升级!

7月18日智东西消息&#xff0c;在2026 WAIC期间&#xff0c;努比亚联合字节豆包打造的二代“豆包手机”努比亚NaviX Ultra首次亮相&#xff0c;相比一代有诸多升级。智能体手机理念中兴通讯终端事业部总裁、努比亚总裁倪飞表示&#xff0c;智能体手机要从人操作手机变为手机帮人…

2026/7/20 0:01:04阅读更多 →
努比亚NaviX Ultra亮相WAIC,智能体手机能否让用户生活更简单?

努比亚NaviX Ultra亮相WAIC,智能体手机能否让用户生活更简单?

努比亚NaviX Ultra&#xff1a;外观与功能双升级在2026 WAIC期间&#xff0c;首次亮相的努比亚NaviX Ultra吸引了众多目光。它是努比亚联合字节豆包打造的二代“豆包手机”&#xff0c;与一代努比亚M153相比&#xff0c;外观设计变化较大。其机身背部搭载横向排布的大尺寸影像模…

2026/7/20 0:01:04阅读更多 →
C# 将逗号分割的字符串转换为long,并添加到List<long>

C# 将逗号分割的字符串转换为long,并添加到List<long>

目录 方法1&#xff1a;使用Split和Convert.ToInt64 方法2&#xff1a;使用LINQ的Select和ToList 方法3&#xff1a;使用TryParse进行异常安全转换&#xff08;推荐&#xff09; 如果您喜欢此文章&#xff0c;请收藏、点赞、评论&#xff0c;谢谢&#xff0c;祝您快乐每一天…

2026/7/20 0:01:04阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时&#xff0c;发现推理速度只有可怜的 1-2 FPS&#xff0c;而别人的演示视频却能跑到 30 FPS 以上&#xff0c;那么问题很可能不在模型本身&#xff0c;而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后&#xff0c;会直接使用官方示例…

2026/7/19 22:50:49阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一&#xff1a;为什么你需要了解 Coze 和 Dify&#xff1f;如果你对 AI 应用开发感兴趣&#xff0c;但一看到“大模型”、“智能体”、“工作流”这些词就头疼&#xff0c;觉得门槛太高&#xff0c;那这篇文章就是为你准备的。很多开发者&#xff0c;包括我自己&#…

2026/7/19 14:50:26阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会&#xff1a;配图一直是个让人头疼的问题。2026年&#xff0c;AI生图工具已经非常成熟了&#xff0c;但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1&#xff1a;速度之王2026年6月11日&#xff0c…

2026/7/19 18:50:36阅读更多 →