C++ SFINAE简介和std::enable_if_t的简单使用

最近整理代码时发现了有人常会使用std::enable_if_t,据说这个是C++14才支持的写法,因此再次勾起了我的整理欲。但要是熟悉std::enable_if的话其实也没啥太大难度,自认为这种使用方式主要提供了一种通过模板偏特化来实现的类型筛选机制,某些情况下在设计复杂工程的泛化处理时能提供一些方便。但能力有限,目前我还没有发现哪些非常典型的使用场景能大幅提升性能。

不过整理之前感觉有必要先引入一个很重要的概念:SFINAE,这是英文Substitution failure is not an error的缩写,意思是匹配失败不是错误。这句话的意思是:我们使用模板函数时编译器会根据传入的参数来推导适配最合适的模板函数,在某些情况下,推导过程会发现某一个或者某几个模板函数推导起来其实是无法编译通过的,但只要有一个可以正确推导并编译出来,则那些推导得到的可能产生编译错误的模板函数就并不会引发编译错误,即匹配失败不是错误。下面举个栗子:

struct testA 
{
	int data;
	testA(int val) :data(val){};
};
struct testB :testA
{
	typedef double value;
	testB(int val) :testA(val){};
};
template<typename T>
typename T::value add(T t1, T t2)	// Definition #1
{
	return t1.data + t2.data;
}
int add(testA t1, testA t2)	// Definition #2
{
	return t1.data + t2.data;
}

从代码编写角度来说,乍一看总感觉模板函数存在有问题:在未明确输入类型是testA还是testB时就贸然使用了其中的value类型,如果是其他场景一般会引起编译器的报错,理论上着实也不甚严谨。 但依托于模板特化的运作机制,编译器进行类型推导时会尝试适配该模板,SFINAE特性会引导编译器在遭遇特化失败后放弃该模板并转向其他函数,且不会报错。
下面时执行结果:

int _tmain(int argc, _TCHAR* argv[]){
	testA a(1), b(2);
	testB c(3), d(4);
	add(a, b);	// Call #2. Without error (even though there is no testA::value) thanks to SFINAE.
	add(c, d);	// Call #1
	return 0;
}

当然,我这个小例子中使用了一些对于结构体struct的派生继承,其实对于C++来说,对于struct的使用也已经提升到了新的层面,其中的使用场景我一会再总结个小文章吧,点击前往

现在开始描述下std::enable_if的使用方式吧,std::enable_if顾名思义,满足条件时类型有效。作为选择类型的小工具,其广泛的应用在 C++ 的模板元编程(meta programming)中。基本实现方式大约为:

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

一个是普通版本的模板类定义,一个偏特化版本的模板类定义。
主要在于第一个参数是否为true,当第一个模板参数为false的时候并不会定义type,只有在第一模板参数为true的时候才会定义type

typename std::enable_if<true, int>::type t; //正确,type等同于int
typename std::enable_if<true>::type; //可以通过编译,没有实际用处,推导的模板是偏特化版本,第一模板参数是true,第二模板参数是通常版本中定义的默认类型即void,但是一般也用不上它。
typename std::enable_if<false>::type; //无法通过编译,type类型没有定义
typename std::enable_if<false, int>::type t2; //同上

网上扒过来了一个用于偏特化的小例子:

template <typename T>
typename std::enable_if<std::is_trivial<T>::value>::type SFINAE_test(T value)
{
    std::cout<<"T is trival"<<std::endl;
}

template <typename T>
typename std::enable_if<!std::is_trivial<T>::value>::type SFINAE_test(T value)
{
    std::cout<<"T is none trival"<<std::endl;
}

下面是一个用于校验函数模板参数类型的小例子:

template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
is_odd(T t) {
  return bool(t%2);
}
 
template <typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even(T t) {
  return !is_odd(t); 
}

到这里对于enable_if_t就更通俗易懂了:

template <bool _Test, class _Ty = void>
using enable_if_t = typename enable_if<_Test, _Ty>::type;

可以直接拿来作为类型来使用,以下是我从网上找的一些简单事例。
1、作为函数参数或返回值:

template<typename T>
struct Check1
{
    //如果T的类型是int,则定义函数 int read(void* = nullptr)
	template<typename U = T>
	U read(typename std::enable_if_t<std::is_same_v<U, int> >* = nullptr) {
		return 42;
	}
    
    //如果T的类型是double,则定义函数 double read()
	template<typename U = T>
	typename std::enable_if_t<std::is_same_v<U, double>, U> read() {
		return 3.14;
	}
}

作为模板参数:

template<typename T>
struct Check2
{
    //如果T的类型是int,则定义函数 int read()
	template<typename U = T, typename std::enable_if_t<std::is_same_v<U, int>, int> = 0>
	U read() {
		return 42;
	}
    
    //如果T的类型是double,则定义函数 double read()
	template<typename U = T, typename std::enable_if_t<std::is_same_v<U, double>>* = nullptr>
	U read() {
		return 3.14;
	}
};

类型偏特化:

// T是其它类型
template<typename T, typename = void>
struct zoo;

// 如果T是整型(我从网上扒代码时原文这里写的浮点,我觉得应该是整型吧,
//这里的std::is_integral_v应该是C++新标准里的特性,但也类似于std::is_integral<T>::value,据说在C++17标准之后了。)
template<typename T>
struct zoo<T, std::enable_if_t<std::is_integral_v<T>>>
{
};

最后查阅到据说C++ 20中通过concepts又做了一些简化:

#ifdef __cpp_lib_concepts
#include <concepts>
#endif

// 如果T是整数类型
template<std::integral T>
void display_concepts_1(T num)
{
}

// 如果T是整数类型
void display_concepts(std::integral auto num)
{
}

// 如果T是浮点数类型
void display_concepts(std::floating_point auto num)
{
}

等价于:

// 如果T是整数类型
template<typename T, typename std::enable_if_t<std::is_integral_v<T>>* = nullptr>
void display_1(T num)
{
}

// 如果T是整数类型
template<typename T>
void display(typename std::enable_if_t<std::is_integral_v<T>>* = nullptr)
{
}

// 如果T是浮点数类型
template<typename T>
void display(typename std::enable_if_t<std::is_floating_point_v<T>>* = nullptr)
{
}

就先写到这里吧,期待下次遇到有意思的问题继续记录。
参考文章:
[1]: https://blog.csdn.net/jeffasd/article/details/84667090
[2]: https://blog.csdn.net/kpengk/article/details/119979733
[3]: https://www.jianshu.com/p/45a2410d4085

### 回答1: `std::enable_if` 是一个用于模板元编程的类型特性,可以根据某些条件来选择是否启用一个模板函数。它通常与函数模板一起使用,以便根据某些条件来选择函数的实现。 下面是一个示例代码,展示如何在函数模板中使用 `std::enable_if`。 ```c++ #include <iostream> #include <type_traits> template<typename T> typename std::enable_if<std::is_integral<T>::value, T>::type my_abs(T num) { return num < 0 ? -num : num; } int main() { int a = -5; double b = -3.14; std::cout << my_abs(a) << std::endl; // 输出 5 // std::cout << my_abs(b) << std::endl; // 编译错误,因为 double 不是整数类型 return 0; } ``` 在上面的代码中,`my_abs` 是一个函数模板,它接受一个类型为 `T` 的参数,并返回 `T` 类型的值。然而,`my_abs` 只能用于整数类型,因为我们使用了 `std::enable_if` 来限制了类型。具体来说,我们使用 `std::is_integral<T>::value` 来判断 `T` 是否是整数类型,如果是,则返回 `T` 类型的值;否则,编译器将无法匹配函数模板,从而报错。 在上面的示例中,我们给 `my_abs` 函数传递一个整数一个浮点数作为参数。对于整数参数,`std::enable_if` 的条件为真,从而选择返回整数类型的值;对于浮点数参数,`std::enable_if` 的条件为假,从而导致编译错误。 注意,在 C++11 中,`std::enable_if` 是通过 SFINAE 技术实现的,因此它只能用于模板函数模板类的实例化。如果在普通函数中使用 `std::enable_if`,则会导致编译错误。 ### 回答2: `std::move`函数是C++标准库中的函数之一,用于将对象的所有权转移给另一个对象。在使用`std::move`时,要注意被移动的对象处于有效状态的前提下,即已分配了资源或有某种内部状态。否则,`std::move`可能会导致未定义的行为。 `std::enable_if`是C++标准库中的类型转换辅助工具,用于在编译时根据条件选择是否启用特定函数的重载。如果条件为真,`std::enable_if`将返回一个特定的类型,使得函数重载匹配成功;而如果条件为假,`std::enable_if`将导致该函数在编译时被忽略,从而避免了函数重载冲突。 下面是一个使用`std::move``std::enable_if`的示例: ```cpp #include <iostream> #include <type_traits> template <typename T> typename std::enable_if<std::is_integral<T>::value, T>::type add(T a, T b) { return a + b; } template <typename T> typename std::enable_if<!std::is_integral<T>::value, T>::type add(T a, T b) { return std::move(a) + std::move(b); } int main() { int a = 1, b = 2; std::cout << add(a, b) << std::endl; // 输出 3 std::string str1 = "Hello", str2 = " World"; std::cout << add(str1, str2) << std::endl; // 输出 "Hello World" return 0; } ``` 上述示例中,定义了一个`add`函数的模板,该函数使用了`std::enable_if`辅助工具。对于整型类型,使用普通的加法;对于其他类型,使用`std::move`将参数转移到临时对象中,并执行加法操作。通过使用`std::enable_if`,我们能够根据参数类型选择适当的重载函数实现,从而实现函数行为的多态性。 ### 回答3: `std::move` 函数是 C++ 标准库中的一个模板函数,它用于将对象转移或者交换所有权。 `std::move` 函数用于将对象的状态或所有权从一个对象转移到另一个对象,通常用于实现移动语义。它接受一个参数,并返回对该参数的右值引用。这意味着 `std::move` 函数可以将传入的对象转换为右值引用,从而允许在移动语义中进行操作。 示例:假设有一个自定义的类 `MyClass`,具有合适的移动构造函数移动赋值运算符。我们可以使用 `std::move` 函数将一个 `MyClass` 类型的对象 `obj1` 的所有权转移到另一个 `MyClass` 类型的对象 `obj2`。 ```cpp MyClass obj1; //... MyClass obj2 = std::move(obj1); ``` 通过调用 `std::move(obj1)`,将 `obj1` 的所有权移动到 `obj2`,`obj1` 将不再持有任何资源。这可以用于避免不必要的拷贝操作,提高程序的性能。 此外,`std::move` 函数还可以与其他 C++ 标准库函数以及用户定义的模板函数一起使用。例如,我们可以使用 `std::move` 结合 `std::enable_if` 实现某些类型的特定行为或重载。 示例:假设我们编写了一个模板函数 `foo`,它接受一个 `T` 类型的参数,并且仅当 `T` 是可移动的时才进行某些操作。我们可以使用 `std::enable_if` `std::move` 实现这个功能。 ```cpp template<typename T> typename std::enable_if<std::is_move_constructible<T>::value, void>::type foo(T&& obj) { T temp = std::move(obj); // 对 temp 进行特定操作 } int main() { MyClass obj; //... foo(std::move(obj)); //... } ``` 在上面的示例中,`std::enable_if` 用于在 `T` 是可移动的情况下,才实例化 `foo` 函数。然后,`std::move` 用于将参数 `obj` 转移为右值引用,并将其赋值给 `temp`,从而实现特定操作。 总结:`std::move` 函数是 C++ 标准库中的一个用于转移对象所有权的模板函数。它可用于实现移动语义,避免不必要的拷贝操作。同时,它可以与其他标准库函数用户定义的模板函数(如 `std::enable_if`)结合使用,实现特定类型的特定行为或重载。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值