C++17 新特性_第一章 C++17 语言特性_if constexpr,类模板参数推导 (CTAD)

本文记录C++17特性之if constexpr和类模板参数推导 (CTAD)。

文章目录

  • [第一章 C++17 语言特性](#第一章 C++17 语言特性)
    • [1.3 if constexpr](#1.3 if constexpr)
      • [1.3.1 实现原理](#1.3.1 实现原理)
      • [1.3.2 实际使用举例](#1.3.2 实际使用举例)
    • [1.4 类模板参数推导 (CTAD)](#1.4 类模板参数推导 (CTAD))
      • [1.4.1 CTAD (Class Template Argument Deduction) 实现原理](#1.4.1 CTAD (Class Template Argument Deduction) 实现原理)
      • [1.4.2 举例说明](#1.4.2 举例说明)

第一章 C++17 语言特性

1.3 if constexpr

C++17之前,如果想在模板函数中根据类型执行不同的逻辑,通常只有两种选择,SFINAE或标签分发

假如要实现一个print函数,根据类型打印信息:

  • 如果是整数,打印 "整数" + 值。
  • 如果是浮点数,打印 "浮点数: " + 值。
  • 其他类型,直接打印值。
    C++11的模板编程的两种的实现方式:
    方式1:标签分发,使用std::true_type 作为函数参数,在编译器进行判断。
cpp 复制代码
    // 标签分发方式:
    template <typename T>  // 函数模板,第二个参数是 true类型
    void process_impl(T value, std::true_type)
    {
        cout << "处理整数: " << value << endl;
    }

    // 处理非整数类型
    template <typename T>  // 函数模板,第二个参数是 false类型
    void process_impl(T value, std::false_type)
    {
        cout << "处理非整数: " << value << endl;
    }

    // 对外接口
    template <typename T>
    void process(T value)
    {
        // 根据类型特性自动选择正确的实现
        process_impl(value, std::is_integral<T>());
        // 如果T不是整数,调用std::false_type类型,
    }
    void test()
    {
        // 处理整数类型
        process(42);
        // 输出: 处理整数: 42
        process(3.14);
        // 输出: 处理非整数: 3.14
    }

C++实现方式2:SFINAE方式实现,总体实现是使用编译时执行的特性作为print的返回值,多个print之间是函数重载。当typename std::enable_if<...>::type为true时,print的函数的返回值为void,函数等同于:

cpp 复制代码
typename std::enable_if<...>::type

这是将SFINAE条件放在返回类型位置的技巧。当表扬你其遇到print()时,执行如下步骤:

1 建立候选集,将所有名为print函数模板作为候选。

2 模板参数推导与替换:根据传入的实际参数类型选择合适的print。如果enable_if<>条件为false,那么::type就不存在,替换失败,这就不是一个合法的函数,因为没有返回值,根据SFINAE特性,将这个函数从候选列表中删除;如果std::enable_if为true, 函数签名有效(即符合函数定义三要素,返回值,函数名称,参数),将选择这个函数。

cpp 复制代码
	// SFINAE方式:
    // 处理整数
    template<typename T>
    typename std::enable_if< std::is_integral<T>::value >::type print(T t)
    {
		std::cout << "整数: " << t << std::endl;
    }

    // 处理浮点数
    template<typename T>
    typename std::enable_if<   std::is_floating_point<T>::value    >::type print(T t)
    {
        std::cout << "浮点数: " << t << std::endl;
	}

    // 其他类型
    template<typename T>
    typename std::enable_if<  !std::is_integral<T>::value && !std::is_floating_point<T>::value  >::type 
        print(T t)
    {
		std::cout << "其他类型: " << t << std::endl;
    }

    void test()
    {
        print(100);
        // 整数: 100
        print(3.14159); 
		// 浮点数: 3.14159
		print("Hello");    // 其他类型
		// 其他类型: Hello
    }

上面两种写法都能实现类型的判断,但是代码量都比较多。C++17可以使用更简单的方式实现上边的类型判断。

1.3.1 实现原理

if constexpr的执行原理是,当编译期间遇到 if constexpr (cond) 时:

1 cond 必须是一个编译期常量表达式(能转为 bool)。

2 如果 cond 为 true,编译器会正常编译 then 块的代码,而 else 块的代码会被丢弃,即不被实例化。

3 如果 cond 为 false,then 块被丢弃,else 块被编译。

1.3.2 实际使用举例

示例1:使用if constexpr重新实现上面两种类型判读的方式。

cpp 复制代码
    template<typename T>
    void print(T t)
    {
        if constexpr (std::is_integral_v<T>)
        {
            std::cout << "整数: " << t << std::endl;
        }
        else if constexpr (std::is_floating_point_v<T>)
        {
            std::cout << "浮点数: " << t << std::endl;
        }
        else
        {
			std::cout << "其他类型: " << t << std::endl;
        }
    }

    void test()
    {
        print(200);
         // 整数: 200
        print(2.71828);
        // 浮点数: 2.71828
        print("World");
		// 其他类型: World
    }

示例2:判断类型是否有 .length()方法

cpp 复制代码
    // 泛化
    template<typename T,typename = void>
    struct has_length : std::false_type
    {

    };

    template<typename T>
    struct has_length<T,std::void_t<decltype(std::declval<T>().length())>> : std::true_type
    {

    };

    template<typename T>
    size_t get_len(const T& t)
    {
        if constexpr (has_length<T>::value)
        {
            return t.length();// 只有当 T 有 length() 时,这行才会被编译
        }
        else
        {
            return sizeof(t);  // 对于 int,只会编译这行
		}
    }

    void test()
    {
        std::string str = "Hello, World!";
        int arr[10];
        std::cout << "字符串长度: " << get_len(str) << std::endl;
        // 字符串长度: 13
        std::cout << "数组大小: " << get_len(arr) << std::endl;
        // 数组大小: 40 
	}

示例3:参数包展开

C++17之前的方式,定义一个递归函数终止函数,一个递归函数。

cpp 复制代码
    // 递归终止函数
    void print_all()
    {
        cout << endl;
    }
	// 递归打印函数
    template <typename T, typename... Args>
    void print_all(T first, Args... args)
    {
        std::cout << first << " ";
        print_all(args...); // 递归调用
    }

    void test()
    {
		print_all(1, 2.5, "Hello", 'A');
        // 1 2.5 Hello A
    }

实现方式2:使用C++17的实现方式,不需要写递归结束函数,直接判断。

cpp 复制代码
	// C++17 之后的方式,使用 if constexpr
    template <typename T, typename... Args>
    void print_all2(T first, Args... args) {
        std::cout << first << " ";

        // 检查参数包是否为空
        if constexpr (sizeof...(args) > 0) {
            print_all(args...); // 只有还有参数时才递归
        }
    }

    void test()
    {
		print_all2(1, 2.5, "Hello", 'A');
        // 1 2.5 Hello A
    }

enable_if的使用,见《C++模板与泛型编程》专栏.

1.4 类模板参数推导 (CTAD)

C++17之前,在推导函数模板和类模板的类型参数时,函数模板可以被编译器自动推导出来,比如:

cpp 复制代码
template <typename T>
void func(T t) {}

func(10); // 编译器自动推导 T 为 int,不需要写 func<int>(10)

但是类模板不能自动推导,需要手动指定类型,假如要实例化std::pair类型对象,写法如下:

cpp 复制代码
std::pair<int, double> p(1, 2);

如果少了<> 尖括号中的类型,将报错。

但是,对于tuple和pair,标准库的开发者为我们提供了make_的辅助函数,比如

cpp 复制代码
		auto p = std::make_pair(1, 2.0);
        auto p2 = std::make_tuple(1, 2.0);

C++中对于类模板,C++中引入了更简单的实例化方式。

1.4.1 CTAD (Class Template Argument Deduction) 实现原理

CTAD (Class Template Argument Deduction) 实现原理是,编译器直接从构造函数的参数中推导出类模板的参数类型。

C++17写法:推导内置类型。

cpp 复制代码
std::pair p(1, 2.0); // 编译器自动推导为 std::pair<int, double>
std::vector v = {1, 2, 3}; // 编译器自动推导为 std::vector<int>

CTAD推导自定义类型,这里有一个问题,有时候,构造函数的参数类型和类模板的参数类型并不直接对应,或者我们需要特殊的推导逻辑(比如将字符串字面量推导为 std::string 而不是 const char*)。这时,我们需要手动写"推导指引"。

语法格式:

cpp 复制代码
类名(构造函数参数) -> 类名<模板参数>;

举例说明:

cpp 复制代码
    template <typename T>
    struct Container {
        T data;
        Container(T t) : data(t) {}
    };

    // 自定义推导指引:
    // 如果构造函数接收 const char*,请推导 T 为 std::string
    Container(const char*)->Container<std::string>;

    void test()
    {
        Container c("hello");
        // 如果没有指引,T 是 const char*
       // 有了指引,T 变成 std::string
    }

1.4.2 举例说明

使用举例。

cpp 复制代码
    void test2()
    {
        // 1. vector
        std::vector v = { 1, 2, 3, 4 };
        // std::vector<int> 

        // 2. tuple
        std::tuple t(10, 3.14, "C++"); 
        // std::tuple<int, double, const char*> // 

        // 3. lock_guard
        std::mutex mtx;
        // std::lock_guard<std::mutex> // C++17之前
        std::lock_guard lk(mtx);  // C++17  不用写 <std::mutex> 了
    }
相关推荐
繁华似锦respect1 小时前
C++ 设计模式之单例模式详细介绍
服务器·开发语言·c++·windows·visualstudio·单例模式·设计模式
小年糕是糕手1 小时前
【C++】类和对象(三) -- 拷贝构造函数、赋值运算符重载
开发语言·c++·程序人生·考研·github·个人开发·改行学it
艾莉丝努力练剑1 小时前
【C++:C++11收尾】解构C++可调用对象:从入门到精通,掌握function包装器与bind适配器包装器详解
java·开发语言·c++·人工智能·c++11·右值引用
徐新帅1 小时前
C++ 竞赛训练营第三课:STL 核心容器之 priority_queue
开发语言·c++
八月的雨季 最後的冰吻1 小时前
FFmepg--29- C++ 音频混音器实现
开发语言·c++·音视频
拾光Ծ1 小时前
“异常”处理机制 与 C++11中超实用的 “智能指针”
java·开发语言·c++·安全
枫叶丹41 小时前
【Qt开发】Qt窗口(五) -> 非模态/模态对话框
c语言·开发语言·数据库·c++·qt
_OP_CHEN1 小时前
算法基础篇:(二十二)数据结构之单调队列:滑动窗口问题的 “最优解” 神器
数据结构·c++·算法·蓝桥杯·算法竞赛·单调队列·acm/icpc
Lenyiin1 小时前
02.05、链表求和
数据结构·c++·算法·leetcode·链表