首页 > 编程语言 >掌握现代C++的模板元编程类型检测技术

掌握现代C++的模板元编程类型检测技术

时间:2024-06-17 10:58:20浏览次数:26  
标签:std void 编程 C++ 类型 hello 模板

最近写代码恰好用到了C++模板元编程的类型检测能力,以前对其原理有个大概的印象,但随着C++11/C++17等新特性的加入,很多做法和以前不同了,借此机会重新梳理一下这方面的知识点。

void_t 的引入

在 C++17 之前,模板编程中通常需要编写复杂的部分特化和重载来检测类型特征。C++17 引入了 std::void_t,简化了这一过程。其定义如下:

template< class... >
using void_t = void;

这个定义看似简单,但实际上它为模板编程打开了新的可能性。

void_t 在 SFINAE 中的应用

SFINAE是Substitution Failure Is Not An Error的缩写,直译为:匹配失败不是错误。属于C++模板编程中的高级技巧,但属于模板元编程中的基本技巧。SFINAE 是一种模板实例化过程中的规则,当模板参数替换失败时,并不会产生编译错误,而是导致模板实例被丢弃,编译器会继续寻找其他匹配的模板实例。

使用 void_t,可以创建一些检测类型特征的工具,例如:

检测类型成员

我们可以编写一个模板结构体来检测一个类型是否包含某个成员类型 type

template <class, class = std::void_t<>>
struct has_type : std::false_type {};

template <class T>
struct has_type<T, std::void_t<typename T::type>> : std::true_type {};
检测成员变量

同样的技巧可以用来检查一个类型是否有某个特定的成员变量 a

template <class, class = std::void_t<>>
struct has_a_member : std::false_type {};

template <class T>
struct has_a_member<T, std::void_t<decltype(std::declval<T>().a)>> : std::true_type {};
检测迭代器

我们还可以检验一个类型是否可迭代,即是否有 begin()end() 方法:

template <typename, typename = void>
constexpr bool is_iterable = false;

template <typename T>
constexpr bool is_iterable<T, std::void_t<decltype(std::declval<T>().begin()), decltype(std::declval<T>().end())>> = true;
检测成员函数

同样可以检查一个类型是否有某个特定的成员函数 hello

template <class T, class = void>
struct has_hello_func : std::false_type {};

template <class T>
struct has_hello_func<T, std::void_t<decltype(std::declval<T>().hello())>> : std::true_type {};

std::declval的作用

std::declval 是一个在 <utility> 头文件中定义的函数模板,它的主要作用是在不实例化对象的情况下获取该类型的引用,以便在编译时期在表达式中使用。这在模板元编程和类型萃取中特别有用,因为它允许我们对某个类型的成员进行操作,而不需要构造实际的对象。

std::declval 通常与 decltype 结合使用,用于推导表达式的类型。它只能在不被求值的上下文中使用(如 decltypesizeof 中),因为它实际上没有定义,只是一个声明。如果在运行时尝试使用 std::declval,将会导致链接错误。

让我们来仔细解释上面出现过的这段代码:

template <class T, class = void>
struct has_hello_func : std::false_type {};

这里定义了一个模板结构体 has_hello_func,它默认继承自 std::false_type。这个结构体的作用是用于检查类型 T 是否有成员函数 hello。这里使用了一个非类型模板参数,其默认值是 void。这是为了利用 SFINAE 规则准备的,如果 T 不满足某些条件,这个基础版本将会被选择。

template <class T>
struct has_hello_func<T, std::void_t<decltype(std::declval<T>().hello())>> : std::true_type {};

这是 has_hello_func 的一个特化版本。它只会在模板参数 T 满足 decltype(std::declval<T>().hello()) 是一个有效表达式的情况下实例化。这个表达式的作用是尝试调用类型 Thello 成员函数,而不实际构造一个 T 的实例。如果该成员函数存在,decltype 将成功推导出其类型,并且 std::void_t<decltype(...)> 将等价于 void,从而使得这个特化版本满足 SFINAE 条件,成为被选择的模板。

如果 T 有成员函数 hello,那么 std::void_t<decltype(std::declval<T>().hello())> 就不会导致替换失败,这个特化版本会被实例化,结构体将从 std::true_type 继承,其 value 成员将是 true。如果 T 没有 hello 函数,那么表达式 std::declval<T>().hello() 会导致替换失败,因此基础版本(继承自 std::false_type)将会被选择,其 value 成员将是 false

C++ 20的做法

随着 C++20 标准的推出,类型检测在 C++ 中进入了一个新的时代。C++20 引入了两个关键特性:Constraints(约束)和 Concepts(概念),它们为类型检测提供了官方的语言支持,极大地简化了模板编程。

Concepts

Concepts 是对模板参数所需特性的正式规定。它们是可编译的规范,定义了类型必须满足的接口和语义要求。Concepts 允许开发者以声明性的方式指定模板参数应该遵循的约束,这使得模板代码更加清晰和容易理解。使用 Concepts,编译器可以提供更清晰的错误信息,因为它可以检查类型是否符合概念的要求,并在不符合时报错。

例如,如果你想要定义一个只接受迭代器类型的模板函数,你可以这样做:

#include <concepts>

template <typename T>
requires std::input_iterator<T>
void myFunction(T iter) {
    // ...
}

或者使用新的语法糖来简化:

template <std::input_iterator T>
void myFunction(T iter) {
    // ...
}

Constraints

Constraints 是概念的实际表达式,它们是概念要求的具体化。Constraints 可以用来指定模板参数必须符合的条件。它们可以是简单的表达式也可以是复杂的布尔逻辑。C++20 中的 requires 表达式用来指定 constraints,提供了一种更简洁和灵活的方式来指定模板参数的要求。

例如,可以使用 requires 表达式来直接在模板参数列表中对类型进行约束:

template <typename T>
requires std::integral<T>
T add(T a, T b) {
    return a + b;
}

这个函数 add 要求模板参数 T 必须是一个整数类型(满足 std::integral 的概念)。

C++20 概念的好处

  • 更清晰的代码:概念使得模板的意图更加明确,代码可读性大大提高。
  • 更好的编译器诊断:当类型不满足模板要求时,编译器可以提供更具体和有用的错误信息。
  • 更好的性能:在某些情况下,因为类型检测更加精确,编译器可以生成更优化的代码。
  • 更简单的类型检测:不再需要编写繁琐的 std::void_t 类型特征模板来检查类型属性,概念本身就定义了类型应该具备的属性。

C++20 的 Constraints 和 Concepts 出现后,开发者可以利用标准库提供的预定义概念,或者定义自己的概念,以更自然、清晰和直观的方式来编写模板代码。

结语

std::void_t 的引入极大简化了模板元编程中的类型检测,通过利用 SFINAE 原则和模板特化优先级,可以编写出既简洁又强大的类型特征检查工具。C++发展到C++20之后,检查类型约束的清晰度和灵活性大大增加,但是目前大部分模板库主流还是使用SFINAE相关技术,也许再过段时间C++20的Constraints(约束)和 Concepts(概念)才会普及。

标签:std,void,编程,C++,类型,hello,模板
From: https://blog.csdn.net/hebhljdx/article/details/139736617

相关文章

  • 现代 C++ 中的一次函数调用的工作流程
    现代C++中的一次函数调用的工作流程ChatGPT4o给的答案:函数声明解析编译器首先解析函数调用,确定要调用的函数。这包括名称查找、重载解析和模板实例化。参数传递编译器检查传递的参数与函数签名是否匹配。如果有隐式类型转换,编译器会进行必要的类型转换。函数调用......
  • 蓝桥杯备考冲刺必刷题(C++) | 3792 小蓝的礼物
    学习C++从娃娃抓起!记录下蓝桥杯备考比赛学习过程中的题目,记录每一个瞬间。附上汇总贴:蓝桥杯备考冲刺必刷题(C++)|汇总-CSDN博客【题目描述】小蓝想要给她的女朋友小桥买一份生日礼物,她来到了一家礼品店。在店里,她看中了N......
  • 开源复刻apple 数学笔记;纯C++实现了ChatGLM系列模型;腾讯混元文生图模型发布新版本并开
    ✨1:AIMathNotesAIMathNotes是一个交互式绘图应用,可绘制并计算数学方程。AIMathNotes受到Apple在WWDC2024上的“MathNotes”演启发,开发的一个互动式绘图应用程序,用户可以在画布上绘制数学方程。一旦方程被绘制完成,应用程序将使用多模态LLM(LargeLanguageM......
  • c/c++设计模式--备忘录模式
    #include<iostream>#include<vector>#ifdef_DEBUG//只在Debug(调试)模式下#ifndefDEBUG_NEW#defineDEBUG_NEWnew(_NORMAL_BLOCK,__FILE__,__LINE__)//重新定义new运算符#definenewDEBUG_NEW#endif#endif//#include<boost/type_index.hpp>usingna......
  • 并发编程理论基础——死锁初阶(四)
    使用细粒度锁可能会导致死锁        死锁:一组互相竞争资源的线程因互相等待,导致永久阻塞的现象如何产生死锁互斥,共享资源X和Y只能被一个线程占用占有且等待,线程T1已经取得了共享资源X,在等待共享资源Y的时候,不释放共享资源X不可抢占,其他线程不能强行抢占线程T1......
  • 结构化绑定(c++17)
    结构化绑定(Structuredbindings)是C++17引入的一个特性,它使得从元组或者其他类型的数据结构中提取元素变得更加方便和直观。它允许我们通过一条语句将一个复杂类型的数据解构成其组成部分,而无需显式地访问每个成员。使用示例:假设有一个结构体Person和一个返回结构体的函数:#i......
  • nii转dicom,需要一个同序列dicom图像模板作为参考
    ``importnibabelimportnumpyasnpimportpydicomimportosfromtqdmimporttqdmdefconvertNsave(arr,file_dir,index=0,slice_thickness=1.0,pixel_spacing=(1.0,1.0)):"""`arr`:parameterwilltakeanumpyarraythatreprese......
  • C/C++ 全局对象注意事项
    在C/C++中,全局对象是指在所有函数外部定义的对象,它们在整个程序生命周期内都是存在的。全局对象有一些特殊的注意事项,下面将详细总结:初始化顺序:全局对象的构造函数在程序开始执行之前就会被调用,这意味着它们会在任何函数(包括main函数)之前被初始化。因此,必须确保全局对象的......
  • 【网络编程开发】17.“自动云同步“项目实践
    17."自动云同步"项目实践文章目录17."自动云同步"项目实践项目简介功能需求需求分析实现步骤1.实现TCP通信server.c服务端tcp.hclient.c客户端函数封装tcp.ctcp.hserver.cclient.c编译运行2.实现文件传输sever.cclient.ctcp.ctcp.hMakeifle编译运行3.实现用文件名......
  • 怎样解决 Bash 与其他编程语言交互时出现的兼容性问题?
    要解决Bash与其他编程语言交互时出现的兼容性问题,可以考虑以下几个方法:使用标准化的输入输出:确保你的Bash脚本与其他编程语言之间使用标准化的输入输出格式进行通信。这可以包括使用标准输入和标准输出进行交互,并使用标准格式(如JSON或CSV)来传递数据。使用跨平台的工具或......