C++ 的 if-constexpr

news2025/3/27 8:51:46

1 if-constexpr 语法

1.1 基本语法

​ if-constexpr 语法是 C++ 17 引入的新语法特性,也被称为常量 if 表达式或静态 if(static if)。引入这个语言特性的目的是将 C++ 在编译期计算和求值的能力进一步扩展,更方便地实现编译期的分支选择动作。早期的 C++ 缺少类似的语言特性,C++ 开发者不得不使用 tag dispatching 这样的惯用手法或者借助于模板特化机制让编译器在模板参数推导时“曲折地”实现一些静态选择。C++ 11 标准明确了 SFINAE 机制的应用,并提供了 enable_if ,二者配合可以更方便地实现编译期的分支选择,但是与 if-constexpr 相比,可读性和易用性都差了几条街。

​ 在介绍 if-constexpr 的“神通”之前,前来看一下 if-constexpr 的语法形式。其实我们在介绍如何让自己的设计的类型支持结构化绑定的时候(《让自定义类型支持结构化绑定》),为实现 get<N>() 成员方法,就用到了这种语法,这里再回顾一下 if-constexpr 语法的表达形式:

struct FooTest
{
    template<std::size_t N>
    const auto& get() const
    {
        if constexpr (N == 0) return name;
        else if constexpr (N == 1) return age;  //else if 分支中的 constexpr 说明符可以省略
        else return weight;
    }
};

这个函数中对模板参数 N 的判断在编译期间进行,由哪个分支返回数据也是在编译期间就决定了,false 分支中的代码甚至不会出现在实例化后的函数代码中。以get<N>()成员函数为例,如果使用 FooTest 的代码用到 get<0>() 取 name 这个成员,则最终实例化后会得get<0>()的特化实现:

struct FooTest
{
    template<>
    const std::string& get<0>() const
    {
        return name;
    }
};

如果代码中还用了get<1>()取 age 的值,则编译器还会实例化出 get<1>()这个特化版本:

template<>
const int& get<1>() const
{
    return age;
}

1.2 扩展说明

1.2.1 条件表达式

​ 常量 if 表达式中的条件表达式必须是 bool 类型的常量表达式,或者是可转换成 bool 类型的常量表达式。因为这个条件表达式是在编译期进行评估的,所以 constexpr 修饰的常量或函数都可以出现在这个表达式中,但是运行期间的变量或非 constexpr 的函数不可以出现在条件表达式中。

​ C++ 17 中,lambda 表达式(lambda 函数)可以被显式声明为 constexpr,但是当一个 lambda 表达式和相关的上下文一起达成一个闭包时,如果这个闭包在一个 constexpr 上下文中使用,即使没有将其显式声明为 constexpr,它也会被当作 constexpr 使用,比如:

auto DoubleIt = [](int n)  {  return n + n;  };
template<std::size_t N>
bool Func2()
{
    if constexpr (DoubleIt(N) < 100)
        return true;
    else
        return false;
}

std::cout << Func2<10>() << ", " << Func2<50>() << std::endl;  //1, 0

1.2.2 false 分支处理

​ 函数模板实例化时,评估为 false 分支的语句不会出现在最终实例化后的函数代码中,但是编译器会对其进行语法检查,当出现语法错误时也会报错(有些编译器报错)。虽然会进行语法检查,但是 false 分支中的 return 语句不会参与函数的返回值类型推定,比如这个例子:

template<typename T>
auto get_value(T t)
{
    if constexpr (std::is_pointer_v<T>)
        return *t; 
    else
        return t; 
}

std::is_pointer_v<T>为 true 时,else 分支中的 return t 语句不参与返回值类型推定,函数返回值类型推定为 *t 的类型。当std::is_pointer_v<T>为 false 时正好相反,else 分支中的 return t 语句将决定返回值类型,函数返回值类型推定为 t 的类型。这里需要注意,如果取消配合 if-constexpr 的 else 分支,改用函数设计常用的默认返回方式,编译器就会报错,比如:

template<typename T>
auto get_value(T t)
{
    if constexpr (std::is_pointer_v<T>)
        return *t; 

    return t; 
}

这个 get_value2() 函数的代码语义与前面的 get_value() 函数一样,但是编译器会报错,因为最后的 return 语句也参与返回值类型的推定,并且当 T 是指针类型的时候,两个 return 语句的返回值类型推定会互相矛盾。

​ 尽管编译器会对 false 分支的代码进行语法检查,但是 false 分支的代码会被丢弃,所以不参与代码链接。比如这个例子:

extern int x; //

int f()
{
    if constexpr (true)
        return 0;
    else if (x)
        return x;
    else
        return -x;
}

f(); //调用 f

尽管全局变量 x 只有一个 extern 声明,并没有定义,但是这段代码编译正常,没有错误。因为 else if 和 else 分支的代码都被丢弃,编译器没有为链接代码而定位 x 的需要。

​ 编译器之所以要对准备丢弃的 false 分支代码进行语法检查,可能原因是它要对整个 if 语句进行分析,了解每个分支逻辑的起始位置和结束位置,以便能够正确地保留 true 分支的代码,丢弃 false 分支的代码。

1.2.3 初始化语句

​ C++ 17 开始支持在 if 语句中使用初始化语句,当然,if-constexpr 语法上也支持初始化语句,只是要求初始化语句也只能使用常量和常量表达式。比如这个例子中的 k 的初始化:

template<std::size_t N>
bool Func()
{
    if constexpr (constexpr std::size_t k = 3; (N % k) == 0)
        return true;
    else
        return false;
}

k 必须是个常量,初始化 k 的表达式必须是常量表达式。

2 if-constexpr 的作用

​ 可在编译期执行的 if-constexpr,用于模板元编程中的条件判断,不仅扩展了模板元编程的分支处理能力,也简化了很多以前用非常复杂方式实现的分支判断代码,使得模板元编程对条件分支的处理代码更直观,更容易理解。这一部分我们用三个例子,分别介绍一下 if-constexpr 的作用,包括对传统的 tag dispatching 和模板特化习惯用法的比较。

2.1 简化可变参数的处理方式

​ 使用 if-constexpr 可以提高泛型代码的可读性,上一节介绍的 get<N>() 的函数,如果不用 if-constexpr,就需要借助于模板的递归推导来解决 N 的个数不确定问题。具体做法就是定义一个泛化版本加上一个特化版本,这样实现起来不仅麻烦,代码可读性也大打折扣。这一节,我们用一个之前介绍过的用于求和的函数模板为例,介绍一下 if-constexpr 对可变参数的处理以及提高代码可读性能带来的好处。

​ 在 C++ 17 之前,没有折叠表达式和 if-constexpr,对参数包的处理需要用到模板的递归推导,需要定义一个结束递归推导的特化实例,非常不直观:

template<typename T>
auto Sum(T arg)
{
    return arg;
}

template<typename T, typename... Args>
auto Sum(T arg, Args... args_left)
{
    return arg + Sum(args_left...);
}

std::cout << Sum(3, 5, 8) << std::endl;  //输出16
std::cout << Sum(std::string("Emma "), std::string(" love cats!")) << std::endl; //输出 Emma love cats!

C++ 17 引入了折叠表达式,用了折叠表达式就简单多了,看看折叠表达式的版本:

template<typename... Args>
auto Sum(Args&&... args)
{
    return (0 + ... + args);
}

但是折叠表达式的语法让很多初学者“毛骨悚然”,如果改成用 if-constexpr 实现,则代码更符合直觉,可读性也上了一个台阶:

template <typename T, typename... Args>
auto Sum(T arg, Args... args)
{
    if constexpr (0 == sizeof...(Args))
        return arg;
    else
        return arg + Sum(args...);
}

2.2 比std::enable_if 更灵活

​ SFINAE (Substitution Failure Is Not An Error)的意思就是模板推导的过程中,如果模板参数替换后得到一个无意义的错误结果,编译器并不立即报错,而是暂时忽略这个模板函数声明,继续参数推导和替换。C++ 11 引入的 std::enable_if 就是实现 SFINAE 的最直接方式,下面用 ToString() 函数为例(注意,这不是一个严谨的实现,只是作为一个例子),看看如何用 std::enable_if 实现编译期的条件分支。

//也可以用 enable_if_t
template<typename T>
std::enable_if<std::is_arithmetic<T>::value, std::string>::type
ToString(T t)
{
    return std::to_string(t);
}

template<typename T>
std::enable_if<!std::is_arithmetic<T>::value, std::string>::type
ToString(T t)
{
    return t;
}

std::to_string() 支持将一个整数型数据或浮点数型数据转成字符串,如果 T 本身就是 std::string 类型,则不需要转换。std::enable_if 的作用就是通过对返回值类型的控制,使得当类型 T 与函数代码不匹配(比如 to_string() 函数不支持 std::string 类型)的时候产生一个错误的函数声明。举个例子,当 T 是 std::string 类型时(不是数字类型),编译器对两个模板函数进行参数替换后得到两个函数声明:

template<>
ToString<std::string>(std::string t);

template<>
std::string ToString<std::string>(std::string t);

第一个替换结果没有函数返回值,是个语法上错误的函数声明,编译器会丢弃这个替换结果,选择第二个语法上正确的作为最终的 ToString() 函数重载裁决结果。如此一来,就通过 std::enable_if 与 SFINAE 机制配合,实现了编译期分支选择的目的。

​ 但是使用 std::enable_if 控制需要注意一点,std::enable_if 只能将条件分割成两种情况,就是两个条件必须互斥,即一个是 true 条件,另一个必须是 false 条件,否则一旦出现两个判断条件都是 true 的情况,就会出现两个正确的结果,导致编译器报告“模棱两可的函数调用” 的编译错误。通过上面的例子可以看出,尽管 std::enable_if 也能实现编译期的条件分支选择,但是代码并不直观,约束条件比较多,且只能实现两个分支的选择。现在看看使用 if-constexpr 的实现方案:

template<typename T>
auto ToString(T t)
{
    if constexpr (std::is_arithmetic<T>::value)
        return std::to_string(t);
    else
        return t;
}

这样的代码要比写两个重载函数让编译器按照 SFINAE 原则匹配调用的方式更直观,也更容易理解和维护。

2.3 比 tag dispatching 更直观

​ tag 就是一些没有数据,没有操作的空类型,它们可以作为函数参数来影响编译器对重载函数的选择。用 tag dispatching 技术首先要定义 tag,根据本文的例子,我们定义两个 tag:

struct NumTag {};
struct StrTag {};

接着要定义重载函数,唯一不同的参数就是 tag 类型,tag 类型作为函数的哑形参只影响编译器对重载函数的选择,最终这个没有的参数都会被编译器优化掉:

template <typename T>
auto ToString_impl(T t, NumTag)
{
    return std::to_string(t);
}

template <typename T>
auto ToString_impl(T t, StrTag)
{
    return t;
}

最后就是实现 ToString(),根据 T 的类型确定是调用 ToString_impl(t, NumTag()); 还是调用 ToString_impl(t, StrTag());,具体做起来就八仙过海,各显神通,比如这个使用自定义 type_traits 的方式:

template <typename T> //一个并不严谨的泛化版本
struct traits
{
    typedef NumTag tag;
};

template <>   //针对 std::string 的特化版本
struct traits<std::string>
{
    typedef StrTag tag;
};

template <typename T>
auto ToString(T t)
{
    return ToString_impl(t, typename traits<T>::tag());  //根据 traits<T>::tag 选择 ToString_impl()
}

​ 对比上一节用 if constexpr 实现的版本,可以看出来使用 tag dispatching 的代码比较晦涩,需要研究一下 tag 的定义才能了解分支选择的具体条件,代码实现不如 if constexpr 直观。

3 if-constexpr 与 if 的区别

3.1 if 为什么不行

​ 上一节的 ToString() 函数的例子如果不用 if-constexpr,像这样直接用 if 实现:

template<typename T>
auto ToString(T t)
{
    if (std::is_arithmetic<T>::value)
        return std::to_string(t);
    else
        return t;
}

是否也可以呢?答案是不可以,因为 std::is_arithmetic<T> 是在编译期求值的,当代码中需要将整数 42 转成字符串,调用 ToString(42) 的时候,传入参数是 int 或 double,评估结果是 true,此时函数模板被实例化成:

auto ToString(int t)
{
    if (true)
        return std::to_string(t);
    else
        return t;
}

这个实例化结果是无法编译的,因为返回值到底是整数还是 std::string 呢?两个 return 语句的返回值类型不一致。再看看到传入参数是 std::string 的情况,此时 if 的评估结果是 false,函数模板被实例化成:

auto ToString(std::string t)
{
    if (false)
        return std::to_string(t);
    else
        return t;
}

尽管走 else 分支,直接返回 t 没有问题,但是 if 分支的编译会有问题,因为 std::to_string() 不支持 std::string 类型。所以,直接使用 if 语句是不可以的。

3.2 if-constexpr 为什么可以

​ 现在对比使用 if-constexpr 的情况。前面提到过,对于 false 分支编译只进行语法分析,不生成代码。所以当代码中出现 ToString(42) 的调用的时候,传入参数是 int,评估结果是 true,此时函数模板被实例化成:

std::string ToString(int t)
{
    return std::to_string(t);
}

当传入参数是字符串类型的时候,else 分支就成为 true 分支被保留,函数模板实例化的结果就是:

std::string ToString(std::string t)
{
    return t;
}

最终实例化的结果和使用 std::enable_if 的结果是一样的,但是语法比 std::enable_if 简单,直观。

4 if-constexpr 与 #if 的区别

​ 编译期 if 表达式很容易让人想到 C++ 的条件编译指令 #if,但是它们的区别还是很明显的,主要有三点:

  • 处理阶段不同:#if 条件编译指令是在代码预处理阶段解析的,预处理器处理完成后提交给编译器时,编译器只能看到 true 分支的内容,而 if-constexpr 的代码都是在编译阶段进行处理的;
  • 条件表达式要求不同:首先是代码处理的阶段不一样,#if 只能使用用于定义的宏、编译器预定义的宏和环境变量,不能使用代码中的函数或变量,而 if-constexpr 的条件表达式可以是代码中的常量,或者常量函数;
  • false 分支的处理方式不同:条件编译中的 false 分支编译器不进行语法检查,实际上它们在预编译阶段就被过滤掉了,而 if-constexpr 中的 false 分支也进行语法检查。

关注作者的算法专栏
https://blog.csdn.net/orbit/category_10400723.html

关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2321444.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

涨薪技术|k8s设计原理

01k8s介绍 Kubernetes 是一个可移植、可扩展的开源平台&#xff0c;用于管理容器化 工作负载和服务&#xff0c;有助于实现声明性配置和自动化。它有一个庞大、快速增长的生态系统。Kubernetes 服务、支持和工具广泛可用。Kubernetes 这个名字起源于希腊语&#xff0c;意思是舵…

基于FPGA的16QAM+帧同步系统verilog开发,包含testbench,高斯信道,误码统计,可设置SNR

目录 1.算法仿真效果 2.算法涉及理论知识概要 2.1 16QAM调制解调原理 2.2 帧同步 3.Verilog核心程序 4.完整算法代码文件获得 1.算法仿真效果 vivado2019.2仿真结果如下&#xff08;完整代码运行后无水印&#xff09;&#xff1a; 设置SNR12db 将FPGA数据导入到MATLAB显…

QuecPython 外设接口之GPIO应用指南

基础知识 了解GPIO基础知识更有益于我们使用它。 框图 GPIO&#xff08;通用输入输出&#xff09;是指一种通用的数字输入/输出接口&#xff0c;用于与外部电子元件或设备进行通信。它通常存在于微处理器、微控制器和其他嵌入式系统中。 物理电路结构如下图所示&#xff1a…

Spring Boot 整合 Nacos 注册中心终极指南

在微服务架构中&#xff0c;配置管理和动态路由是核心需求。Nacos 作为阿里巴巴开源的动态服务发现、配置管理和服务管理平台&#xff0c;能够帮助开发者实现配置热更新、多环境共享配置以及动态路由管理。本文将结合 Spring Boot 和 Spring Cloud Gateway&#xff0c;手把手教…

SQLServer列转行操作及union all用法

1.创建测试表及数据sql如下 create table ScoresTable( Name varchar(50), ChineseScore int, MathScore int ) insert into ScoresTable values(小张,90,95) insert into ScoresTable values(小王,98,99) 2.表中查询结果如下 3.现需列转行显示&#xff0c;每行显示 姓名…

【GL010】C++

1.C中的const关键字有哪些用法&#xff1f; 1.修饰变量&#xff1a;表示变量的值不可修改。 const int a 10; 2.修饰指针&#xff1a; const int* p&#xff1a; // 指针指向的内容不可修改。 int* const p&#xff1a; // 指针本身不可修改。 const int* const…

(Arxiv-2025)MagicDistillation:用于大规模人像少步合成的弱到强视频蒸馏

MagicDistillation&#xff1a;用于大规模人像少步合成的弱到强视频蒸馏 paper是HKUST发布在Arxiv 2025的工作 paper title&#xff1a;MagicDistillation: Weak-to-Strong Video Distillation for Large-Scale Portrait Few-Step Synthesis Project page&#xff1a;地址 Abst…

Excel(进阶篇):powerquery详解、PowerQuery的各种用法,逆透视表格、双行表头如何制作透视表、不规则数据如何制作数据透视表

目录 PowerQuery工具基础修改现有数据理规则PowerQuery抓取数据的两种方式多文件合并透视不同表结构多表追加数据透视追加与合并整理横向表格:逆透视 数据用拆分工具整理数据算账龄 不等步长值组合合并文件夹中所有文件PowerQuery处理CSV文件双行表头、带合并单元格如何做数据…

Simple-BEV的bilinear_sample 作为view_transformer的解析,核心是3D-2D关联点生成

文件路径models/view_transformers 父类 是class BiLinearSample(nn.Module)基于https://github.com/aharley/simple_bev。 函数解析 函数bev_coord_to_feature_coord的功能 将鸟瞰图3D坐标通过多相机&#xff08;针孔/鱼眼&#xff09;内外参投影到图像特征平面&#xff0…

同一个局域网的话 如何访问另一台电脑的ip

在局域网内访问另一台电脑&#xff0c;可以通过以下几种常见的方法来实现&#xff1a; ‌直接通过IP地址访问‌&#xff1a; 首先&#xff0c;确保两台电脑都连接在同一个局域网内。获取目标电脑的IP地址&#xff0c;这可以通过在目标电脑上打开命令提示符&#xff08;Windows系…

基于SpringBoot的名著阅读网站

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

Excel(实战):INDEX函数和MATCH函数、INDEX函数实战题

目录 经典用法两者嵌套查值题目解题分析 INDEX巧妙用法让数组公式&#xff0c;自动填充所有、有数据的行/列INDEX函数和SEQUENCE函数 经典用法两者嵌套查值 题目 根据左表查询这三个人的所有数据 解题分析 INDEX函数的参数&#xff1a;第1个参数是选定查找范围&#xff0c…

uniapp超简单ios截屏和上传app store构建版本方法

​ 假如使用windows开发ios的应用&#xff0c;上架的时候&#xff0c;你会发现&#xff0c;上架需要ios应用多种尺寸的ios设备的截图&#xff0c;和需要xcode等工具将打包好的ipa文件上传到app store的构建版本。 大部分情况下&#xff0c;我们的公司都没有这么多款ios设备来…

Netty源码—5.Pipeline和Handler一

大纲 1.Pipeline和Handler的作用和构成 2.ChannelHandler的分类 3.几个特殊的ChannelHandler 4.ChannelHandler的生命周期 5.ChannelPipeline的事件处理 6.关于ChannelPipeline的问题整理 7.ChannelPipeline主要包括三部分内容 8.ChannelPipeline的初始化 9.ChannelPi…

MySQL小练习

目录 一、单表查询 二、多表查询 一、单表查询 素材&#xff1a; 表名&#xff1a;worker-- 表中字段均为中文&#xff0c;比如 部门号 工资 职工号 参加工作 等 CREATE TABLE worker ( 部门号 int(11) NOT NULL, 职工号 int(11) NOT NULL, 工作时间 date NOT NULL, 工资 float…

Apache Hive:基于Hadoop的分布式数据仓库

Apache Hive 是一个基于 Apache Hadoop 构建的开源分布式数据仓库系统&#xff0c;支持使用 SQL 执行 PB 级大规模数据分析与查询。 主要功能 Apache Hive 提供的主要功能如下。 HiveServer2 HiveServer2 服务用于支持接收客户端连接和查询请求。 HiveServer2 支持多客户端…

检波、限幅、钳位电路

检波电路&#xff1a; 类似调制收音机信号&#xff1a;输入的基波和载波叠加成调制信号&#xff08;信号需要长距离里传输&#xff0c;频率要高&#xff0c;M级别的频率&#xff0c;所以要把低频信号叠在高频信号&#xff0c;才能把低频信号长距离传输&#xff0c;最后到达接收…

学习threejs,使用TextGeometry文本几何体

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.TextGeometry1.1.1 ☘…

Go红队开发—CLI框架(一)

CLI开发框架 命令行工具开发&#xff0c;主要是介绍开发用到的包&#xff0c;集成了一个框架&#xff0c;只要学会了基本每个人都能开发安全工具了。 该文章先学flags包&#xff0c;是比较经典的一个包&#xff0c;相比后面要学习的集成框架这个比较自由比较细化点&#xff0…

高效团队开发的工具与方法 引言

引言 在现代软件开发领域&#xff0c;团队协作的效率和质量直接决定了项目的成败。随着项目规模的扩大和技术复杂度的增加&#xff0c;如何实现高效团队开发成为每个开发团队必须面对的挑战。高效团队开发不仅仅是个人技术能力的简单叠加&#xff0c;更需要借助合适的工具和方…