从 enable_if 了解模板元编程

news2024/9/27 7:26:42

前言

在阅读学习 ZLToolKit 源码时,从如下一段代码中了解到 enable_if 和 SFINAE 的概念,从而引入了对模板元编程的了解。

template<class R, class... ArgTypes>
class TaskCancelableImp<R(ArgTypes...)> : public TaskCancelable {
public:
    using Ptr = std::shared_ptr<TaskCancelableImp>; 
    using func_type = std::function<R(ArgTypes...)>;

    ~TaskCancelableImp() = default;

    template<typename FUNC>
    TaskCancelableImp(FUNC &&task) {
        _strongTask = std::make_shared<func_type>(std::forward<FUNC>(task));
        _weakTask = _strongTask;
    }

    // 任务是可取消的
    void cancel() override {
        _strongTask = nullptr;
    }

    operator bool() {
        return _strongTask && *_strongTask;
    }

    void operator=(std::nullptr_t) {
        _strongTask = nullptr;
    }

    R operator()(ArgTypes ...args) const {
        auto strongTask = _weakTask.lock();
        if (strongTask && *strongTask) {
            return (*strongTask)(std::forward<ArgTypes>(args)...);
        }
        return defaultValue<R>();
    }

    template<typename T>
    static typename std::enable_if<std::is_void<T>::value, void>::type
    defaultValue() {}

    template<typename T>
    static typename std::enable_if<std::is_pointer<T>::value, T>::type
    defaultValue() {
        return nullptr;
    }

    template<typename T>
    static typename std::enable_if<std::is_integral<T>::value, T>::type
    defaultValue() {
        return 0;
    }

protected:
    std::weak_ptr<func_type> _weakTask;
    std::shared_ptr<func_type> _strongTask;
};

对于上述示例,重点关注36至50行的代码中 enable_if 的用法。下面按照 “简单了解模板元编程的概念”、“引入 SFINAE 的概念”,“结合 enable_if 来说明 SFINAE 和模板元编程” 的顺序组织下文。

模板元编程

模板元编程(Template Metaprogramming,简称TMP)是编写 template-based C++程序并执行与编译期的过程。所谓 Template Metaprogram(模板源程序)是以C++编写、执行于C++编译器内的程序。 --《Effective C++》

一句话概括模板元编程,使用C++编写基于模板的程序。

下面通过一个例子来进一步了解一下模板元编程,示例来源 < C++新标准011:模板元编程敲门砖:搞懂SFINAE>。代码示例通过注释进行描述。

// 省略一些必要的头文件

// 下面定义了3个重载函数len,其中有两个是函数模板

// 定义了一个接收原始数组为形参的函数模板
template<class T, unsigned N>
std::size_t len(T(&arr)[N])
{
    std::cout << "std::size_t len(T(&arr)[N])" << std::endl;
    return N;
}

// 定义了一个函数模板,针对 STL 中的容器进行模板特化。
// 对于STL中的容器,都定了一个 size_type 类型和一个 size() 成员函数
template<class T>
typename T::size_type len(const T& t)
{
    std::cout << "T::size_type len(T const& t)" << std::endl;
    return t.size();
}

// 接收任意参数的方法
size_t len(...)
{
    std::cout << "unsigned len(...)" << std::endl;
    return 0;
}

int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    std::cout << len(a) << std::endl;   
    std::vector<int> nums{1, 2, 3, 4, 5};
    std::cout << len(nums) << std::endl;
    std::cout << len(3.14) << std::endl;
}

编译运行的输出结果为:

std::size_t len(T(&)[N])
5
T::size_type len(T const& t)
5
size_t len(...)
0

对上述代码和运行结果进行简单的说明:

  • 由运行结果可知,len(a) 实例化选择了第一个函数模板;len(nums) 实例化选择了第二个函数模板;len(3.14) 实例化选择了第三个函数。这涉及函数模板的重载决议,稍后会简单介绍,这里先暂时跳过。
  • 上述程序中的模板元编程体现在第一个和第二个函数模板,以第一个函数模板为例,编写了一个接收任意类型的原始数组作为参数的函数模板,在程序编译过程中,会根据传入 len 函数的变量来推导出形参类型,从而生成对应类型的代码。注意这一步是在程序编译过程中进行的。像这种编写的简单的函数模板,就是一种简单的模板元编程,即编写的源程序是用于编译器在编译阶段执行。当然这个示例只展示了一种最简单的模板元编程,在实际的模板元编程中,可以在编译阶段做很多事情,例如根据类型的不同进行不同的编译。本文介绍的 enable_if 就是C++标准库中提供的一种模板元编程的模板类,下文会详细介绍这个模板类。

下面对上述示例进行一些变化,来引出 SFINAE 的概念。发生变化的部分添加了对应的注释。

// 添加了一个自定义类,注意,这个自定义类中虽然定义了size_type的别名,但没有定义size成员函数
// 因此当对这个类对象调用len函数时,选择的重载版本应该是第三个非模板函数 len(...)
// 但事实并非如此
class MyClass {
public:
    using size_type = size_t;
};

template<class T, unsigned N>
std::size_t len(T(&)[N])
{
    std::cout << "std::size_t len(T(&)[N])" << std::endl;
    return N;
}

template<class T>
typename T::size_type len(const T& t)
{
    std::cout << "T::size_type len(T const& t)" << std::endl;
    return t.size();
}

size_t len(...)
{
    std::cout << "size_t len(...)" << std::endl;
    return 0;
}

int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    std::cout << len(a) << std::endl;   

    std::vector<int> nums{1, 2, 3, 4, 5};
    std::cout << len(nums) << std::endl;
    std::cout << len(3.14) << std::endl;

    // 自定义类型作为实参调用len函数
    MyClass my;
    std::cout << len(my) << std::endl;
}

编译运行的结果为:
在这里插入图片描述

由编译结果可知,len(my) 选择了第二个函数进行实例化,但因为 MyClass 类中没有定义 size 成员函数,因此最终编译不通过。一个疑惑产生,len(my) 既然实例化第二个函数模板最终会导致编译失败,那为什么在函数重载时不直接选择第三个函数?这就涉及在重载决议(Overload Resolution)了。重载决议的规则比较琐碎,本文只进行简单的介绍,具体细节可查看给出的参考资料。下面通过了解重载决议来理解为什么编译器的行为是这样选择的,然后就能较好的理解 SFINAE 的概念了。

重载决议的过程按如下步骤进行:

  • 构建候选函数集。
  • 确定可行的候选函数。
  • 分析候选集中的函数,以确定最佳匹配函数。

下面结合上述例子进行分析。

  • 当编译器执行 len(my) 这行代码时,因为名为 len 的函数有三个,因此将这三个函数构建为 len 函数调用的候选函数集。
  • 确定候选集后,编译器通过实参类型推导,将模板参数 T 进行实例化,发现第一个函数模板是不匹配,因为第一个函数模板接收的参数类型为数组类型。这里需要注意一点:在确定可行的候选函数这一步,对于函数模板,只对函数的声明进行匹配,对其函数体中的定义不进行匹配展开。因此这一步中,确定的可行的候选函数为第二函数模板和第三个非模板函数。
  • 确定了可行的候选函数后,需要进一步分析候选集中的函数,以确定最佳匹配函数。以上述为例,可行的候选函数为第二个模板函数和第三个非模板函数,这两个函数都可以作为 len(my) 函数语句进行调用,但编译器需要选择最合适的一个进行调用。重点来了,编译器选择最合适函数的依据是什么?答:编译器通过函数的形参类型进行最佳匹配,而与函数体中的定义无关。最佳匹配的细节不在本文展开,具体可查阅相关资料。在上述例子中,第二模板函数实例化推导后的形参类型完全匹配,因此最终编译器对 len(my) 函数调用的最佳匹配函数为第二个模板函数的实例化版本。
  • 因为确定的最佳匹配函数是第二个模板函数,然后编译过程中对其进行实例化展开时,发现 MyClass 类中没有定义 size 成员函数,最终导致了编译不通过。

以上就是结合本例,当遇到函数模板时,对重载决议过程的分析。

我们进一步分析,会发现,上述编译失败是因为在模板函数中,实参替换形参后,被选择为了最佳匹配函数,而这种替换没有考虑函数体中的定义,导致了最终的编译失败。那一个自然的疑问是,既然这种替换失败会导致编译错误,那有没有说明方法让这种模板替换失败不作为一种错误,而是将这替换失败的函数模板从函数候选集中剔除出去。答案是,有的。这就引出了模板元编程中的 SFINAE 概念。下面就来了解 SFINAE 的概念,以及如何实现替换失败而不报错。

SFINAE

SFINAE,“Substitution Failure Is Not An Error”。“替换失败不是错误”。

This rule applies during overload resolution of function templates: When substituting the explicitly specified or deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error. This feature is used in template metaprogramming.

编译器在模板函数重载过程中,会使用实参类型对模板函数的声明做替换,注意,这里只对函数模板声明做替换,而不对函数体中的定义做替换。这里还需要认识到一点,即使函数模板被实参替换后通过函数声明可以作为可行的候选函数集,也不能说明该替换后的函数就是可行的,因为还没有对模板函数的函数体进行校验。当对模板函数进行展开校验后,若发现该函数是调用是不正确(如上述的例子所示),则把该函数从候选集中剔除出去,这就是模板元编程中的 SFINAE。实现 SFINAE 需要一些 ”编程技巧“。下面对上述示例进行修改,以实现 SFINAE。

// 将示例中的第二函数模板修改为如下所示,其余不变
template<typename T>
decltype(T().size(), typename T::size_type()) len(T const& t)
{
	std::cout << "decltype(T().size(), typename T::size_type()) len(T const& t)" << std::endl;
    return t.size();
}

编译运行结果如下所示:

std::size_t len(T(&)[N])
5
decltype(T().size(), typename T::size_type()) len(const T& t)
5
size_t len(...)
0
size_t len(...)
0

由运行结果可知,len(my) 重载决议的最终结果为第三个非模板函数。

下面来分析一下修改后的代码,看看它是如何实现 SFINAE 的。

  • 首先,最核心的修改为 len 函数的返回类型的定义,使用 decltype 类型推导表达式替换了 size_t 。
  • decltype 表达式中使用逗号运算符执行了两条语句,而逗号运算符用于将多个表达式组合在一起,它按照从左到右的顺序依次计算这些表达式,并返回最后一个表达式的值。因此上述示例中,decltype 推导的结果等于 typename T::size_type
  • 因此,修改后的len函数模板和修改前在函数声明上其实是等价的。不同之处在于,在上述的 decltype 表达式中,还执行了 T().size() 语句,且执行顺序是在 typename T::size_type() 语句的前面。而在执行 T().size() 语句时,会进行实参替换,替换为 MyClass().size(),而 MyClass 类中是没有定义 size 成员方法的,因此该替换是失败的。通过这种编程技巧,相当于我们提前告诉了编译器,你若将该函数模板进行实例化时,实例化的对象是MyClass时,替换后该函数是不匹配的,因此就不将其加入到可行的候选函数集中。

上述 SFINAE 的实现技巧是不是还是略显复杂?针对上例,我们可以使用C++标准库提供的 enable_if 来实现 SFINAE。

使用 enable_if 修改后代码如下:

template<typename T, typename T2 = typename std::enable_if<!std::is_same<T,MyClass>::value>::type>
typename T::size_type len(const T& t)
{
    return t.size();
}

下面来看看 enable_if 和 is_same 是如何使用,并分析上述修改后的代码是如何实现 SFINAE 的。

enable_if

enable_if 的实现等价于如下代码,这里就不扒源码了。。。

template<bool B, class T = void>
struct enable_if {};
 
template<class T>
struct enable_if<true, T> { typedef T type; };

有上述代码可知,enable_if 的实现非常简单,有一个模板和一个模板偏特化组成。其作用也比较简单,当模板参数 B 为 true 时,其类中定义了一个 type 成员,等价于形参类型T;当模板参数B为 false 时,其表现为一个空类。其中的模板参数 T 可以省略,省略后其表示为 void 类型。

单纯的看 enable_if 的实现,确实不好想到它有什么用途。实际上,enable_if 常常结合 type_traits 头文件中的其它模板类来使用,例如上面示例中出现的 is_same。下面先来看看 is_same 的用途。

is_same 的声明如下所示,也是一个模板类。在其模板类中定义了一个成员变量 value,当 T 和 U是相同的类型时,value 为 true,否则 value 为 true。(is_same在这就不过解释了)

template< class T, class U >
struct is_same;

回到修改后的 enable_if 代码,当使用实参 my 推导模板形参时,T被实例化为 MyClass 类型,而对 typename T2 = typename std::enable_if<!std::is_same<T,MyClass>::value>::type>,我们来看下其表现得含义。std::is_same<T,MyClass> 判断 T 和 MyClass 指向的是同一类型,因此其 value 成员为 true,因此 !std::is_same<T,MyClass>::value 该表达式为 false,从而 std::enable_if 不具有成员变量 type。因此,对于将 MyClass 作为实参类型对该函数模板进行形参替换会失败,从而不会作为可行的候选函数集。因此,对于 len(my) 最终选择的函数版本为非函数模板的 len(...)

上述写法虽然可以使得的对 MyClass 类型调用 len 函数时,可以触发 SFINAE,但任然存在很大局限性。因为对于函数体内部调用size方法的模板函数,我们是希望其针对 STL 中的容器进行模板特化,上述的修改代码虽然使得 MyClass 类型调用 len 方法能够触发 SFINAE,但仅限于MyClass类型,若存在许多和MyClass类相似但不同的类,该写法就不成立。我们可以改进上述写法,使用模板元编程的技巧来满足所有不是STL容器的自定义类型能够正确触发 SFINAE,这里不再继续展开。

小结一下。enable_if 和 is_same 都是模板类,使用这类的模板类可以使得我们有选择的控制模板实例化,从而实现模板元编程中的 SFINAE 的特性。


最后来看一下前言中提到的如下代码。

// 当 T 被实例化为 void 类型,其对应的函数实例化为该版本
template<typename T>
static typename std::enable_if<std::is_void<T>::value, void>::type
defaultValue() {}

// 当 T 被实例化为 指针 类型,其对应的函数实例化为该版本
template<typename T>
static typename std::enable_if<std::is_pointer<T>::value, T>::type
defaultValue() {
	return nullptr;
}

// 当 T 被实例化为 整型 类型,其对应的函数实例化为该版本
template<typename T>
static typename std::enable_if<std::is_integral<T>::value, T>::type
defaultValue() {
	return 0;
}

理解了模板元编程中的 SFINAE 特性后和 enable_if 的用法后,上述代码的用途就很清晰了。具体见注释,不再展开。

总结

  • 模板元编程即编写特定于模板的程序,让编译器在编译阶段生成特化后的程序。一种常见的模板元编程的场景是,根据模板参数实例化的类型,有选择的生成对应的实例化程序。更多模板元编程的工具类,在 type_traits 头文件中。
  • SFINAE 是模板元编程中的一种特性,当涉及模板重载时,模板实例化后的函数声明不一定是可以的,通过 enable_if 等模板类,可以在模板实例化后”提前“知道实例化后的函数是否正确,从而将不可行的函数剔除出函数重载过程中的候选函数集,避免编译失败的情况。

参考资料

https://en.cppreference.com/w/cpp/language/sfinae
C++新标准011:模板元编程敲门砖:搞懂SFINAE
https://en.cppreference.com/w/cpp/language/overload_resolution

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

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

相关文章

PHP基础 - 循环与条件语句

循环语句 1)for循环: 重复执行一个代码块指定的次数。 for ($i = 0; $i < 5; $i++) { // 初始化 $i 为 0,每次循环后将 $i 值增加 1,当 $i 小于 5 时执行循环echo "The number is: $i \n"; // 输出当前 $i 的值并换行 }// 循环输出结果为: // The number …

【一秒梵高】基于OpenCV4实现图像九种风格迁移

风格迁移 图像风格迁移、色彩填充与色彩变换等&#xff0c;严格意义上来说都属于计算机视觉任务中图像处理的分支。它们输入的是图像&#xff0c;输出的也是图像&#xff0c;过程实现图像到图像的内容与风格的转换&#xff0c;深度学习在这类图像处理任务上也取得了良好的效果…

改进了编排控制并增强了推理的可视性,Agents for Amazon Bedrock 现已上市

七月份的时候&#xff0c;我们推出了 Agents for Amazon Bedrock 预览版。如今&#xff0c;Agents for Amazon Bedrock 全面上市。 Agents for Amazon Bedrock 通过编排多步任务&#xff0c;有助于您加速生成人工智能 &#xff08;AI&#xff09; 应用程序的开发。代理使用基础…

【ARM Coresight 系列 2 文章 -- Trace32 对 APBIC 地址的配置 介绍】

请阅读【ARM Coresight SoC-400/SoC-600 专栏导读】 文章目录 APBIC RomtableTrace32 RESBREAKTrace32 ENRESETAPBIC Romtable 图 1 APBIC 网络图 如上图所示,如果想通过Trace32/DS-5 去访问 AP, 这个时候需要怎么做呢?可以看到 APBIC 中ROMTABLE 中 APB-AP 的偏移是0x002000…

2023 re:Invent|Amazon Q与Amazon CodeWhisperer面向企业开发者提效利器

本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 2023年&#xff0c;以GPT为代表的生成式AI引爆了新一轮技术热潮&#xff0c;短短一年的时…

Jenkins离线安装部署教程简记

前言 在上一篇文章基于Gitee实现Jenkins自动化部署SpringBoot项目中&#xff0c;我们了解了如何完成基于Jenkins实现自动化部署。 对于某些公司服务器来说&#xff0c;是不可以连接外网的&#xff0c;所以笔者专门整理了一篇文章总结一下&#xff0c;如何基于内网直接部署Jen…

【数据结构】栈和队列超详解!(Stack Queue)

文章目录 前言一、栈1、栈的基本概念2、栈的实现&#xff08;数组实现&#xff09;3、栈的基本操作3.1 栈的结构设计3.2 栈常见的基本函数接口 4、栈的实现4.1 初始化栈4.2 栈的销毁4.3 入栈4.4 出栈4.5 判空4.6 长度4.7 获取栈顶元素 完整代码Stack.hStack.cTest.c 二、队列1、…

排序-归并排序与计数排序

文章目录 一、归并排序1、概念2、过程3、代码实现4、复杂度5、稳定性 二、 计数排序1、思路2、代码实现3、复杂度&#xff1a;4、稳定性 一、归并排序 1、概念 是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已…

车载导航系统UI界面,可视化大屏设计(PS源文件)

大屏组件可以让UI设计师的工作更加便捷&#xff0c;使其更高效快速的完成设计任务。现分享车载导航系统科技风蓝黑简约UI界面、车载系统UI主界面、车载系统科技风UI界面、首页车载系统科技感界面界面的大屏Photoshop源文件&#xff0c;开箱即用&#xff01; 若需 更多行业 相关…

数据库动态视图和存储过程报表数据管理功能设计

需求&#xff1a;需要将ERP的报表数据挪到OA中&#xff0c;但是OA表单设计不支持存储过程动态传参&#xff0c;所以需要设计一个系统&#xff0c;可以手动配置&#xff0c;动态显示原本ERP的报表数据&#xff0c;ERP报表是存在数据库的视图和存储过程中 思路&#xff1a;因为E…

算法复习——6种排序方法的简单回顾

算法复习——6种排序方法的简单回顾 常见排序方法&#xff1a;冒泡排序、选择排序、插入排序、堆排序、归并排序、快速排序的简单回顾 冒泡排序 重复“从序列右边开始比较相邻两个数字的大小,再根据结果交换两个数字的位置” 在冒泡排序中&#xff0c;第 1 轮需要比较 n - 1…

整理b站黑马程序员C++课程中对于计算机视觉学习有所帮助的知识点。(重点用*标出)

文章目录 1、注释2、变量3、常量4、标识符5、整型 浮点型 字符型 字符串 布尔6、输入 输出7、逻辑运算法8、 程序流程结构9、三目运算符10、switch语句11、循环语句12、跳转语句13、*数组13.1一维数组名 14、二维数组15、**函数15.1、函数的调用15.2、函数的声明15.3、函数份文…

Android camera的metadata

一、实现 先看一下metadata内部是什么样子&#xff1a; 可以看出&#xff0c;metadata 内部是一块连续的内存空间。 其内存分布大致可概括为&#xff1a; 区域一 &#xff1a;存 camera_metadata_t 结构体定义&#xff0c;占用内存 96 Byte 区域二 &#xff1a;保留区&#x…

HarmonyOS--基础组件TextInput

TextInput 官方文档 TextInput组件https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-basic-components-textinput-0000001427584864-V3#ZH-CN_TOPIC_0000001523968610__%E5%AD%90%E7%BB%84%E4%BB%B6 文本输入框组件 接口 TextInput(value?:…

【Python】用Python发邮件

准备工作 以新浪邮箱为例&#xff0c;进入账号管理&#xff0c;打开授权码并保存下来 用到的包 import smtplib from email.header import Header from email.mime.text import MIMEText 账号授权码准备 这里用的是前面记录的授权码&#xff0c;不是登录密码哦 email_hostsm…

40G AOC线缆全系列产品知识详解

40G AOC&#xff08;Active Optical Cable&#xff09;线缆作为高速数据传输的重要组成部分&#xff0c;在现代通信和数据中心应用中扮演着重要角色。本期文章我们将从其基本原理、应用领域、优势特点等方面对ETU-LINK 40G AOC全系列产品进行解析。 一、40G AOC全系列产品解析…

Facebook广告投放常见错误

在进行Facebook广告投放时&#xff0c;很容易犯一些常见的错误。这些错误可能导致广告投资的浪费&#xff0c;影响广告效果并降低回报。本文小编讲一些常见的Facebook广告投放错误&#xff0c;以及如何避免它们。 1、不明确目标受众 广告的成功与否很大程度上取决于你选择的目…

基于Java+Swing+mysql学生选课成绩信息管理系统

基于JavaSwingmysql学生选课成绩信息管理系统 一、系统介绍二、功能展示三、项目相关3.1 乱码问题3.2 如何将GBK编码系统修改为UTF-8编码的系统&#xff1f; 四、其它1.其他系统实现 五、源码下载 一、系统介绍 学生教师信息管理、年级班级信息管理、课程信息管理、选课、成绩…

怎么制作一个微信小程序商城

随着移动互联网的普及&#xff0c;越来越多的商家开始关注线上销售。微信小程序商城作为一种便捷、实用的线上销售平台&#xff0c;受到了广大商家的青睐。本文将详细介绍如何制作一个微信小程序商城。 一、登录乔拓云平台进入后台 首先&#xff0c;我们需要登录乔拓云平台&am…

亚信科技AntDB数据库——深入了解AntDB-M元数据锁的相关概念

AntDB-M在架构上分为两层&#xff0c;服务层和存储引擎层。元数据的并发管理集中在服务层&#xff0c;数据的存储访问在存储引擎层。为了保证DDL操作与DML操作之间的一致性&#xff0c;引入了元数据锁&#xff08;MDL&#xff09;。 AntDB-M提供了丰富的元数据锁功能&#xff…