本文记录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> 了
}