——《C++Templates》

完美转发(Perfect Forwarding)

std::forward

完美转发(perfect forwarding)是指在函数模板中通过保留传递给模板的参数的值类别(左值或右值)来转发它们,从而将参数完整地传递给另一个函数。完美转发的主要目的是在不丢失参数的值类别的情况下,将参数传递给其他函数,以保持传递参数的原始形式;

其一般和移动语义相结合,用于右值类型的传递,关键字为forward

#include <iostream>
#include <utility>

void func(int&& a) {
	std::cout << "This is int&&." << std::endl;
	std::cout << a << std::endl;
}

void func(int& a) {
	std::cout << "This is int&." << std::endl;
	std::cout << a << std::endl;
}

int main(void) {
	int&& a = 1;

	func(a);
	std::cout << "***************" << std::endl;

	func(std::forward<int>(a));

	return 0;
}

这里如果不使用std::forward声明,参数a传递给func时会自动退化成为左值引用,关键字std::forward用于保持其右值属性;

std::move

move关键字用于移动语义的实现,其用于将一个左值类型转换成一个右值类型,所以我们也可以使用move实现对右值重载函数的调用:

func(std::move(a));

特殊成员函数模板

 无模板的成员函数实例如下:

#include <iostream>
#include <string>

class Person {
private:
	std::string _name;
public:

	Person(const std::string& name)
		:_name(name)
	{
		std::cout << "Person constructor(left_ref) function std::string." << std::endl;
	}
	Person(std::string&& name)
		:_name(name)
	{
		std::cout << "Person constructor(right_ref) function std::string." << std::endl;
	}


	Person(const Person& p) 
		:_name(p._name)
	{
		std::cout << "Person(const Person&) left_ref constructor." << std::endl;
	}

	Person(Person&& p) noexcept
		:_name(p._name)
	{
		std::cout << "Person(const Person&) right_ref constructor." << std::endl;
	}


	~Person() {
		std::cout << "~Person()" << std::endl;
	}
};

int main(void) {
	Person p(std::string("a"));
	Person copy_p(p);
	Person move_p(std::move(p));

	return 0;
}

第一个实例,使用了std::string的构造函数的返回值,属于右值类型,所以匹配右值引用调用;

第二个实例,由于p是一个左值类型,所以调用左值引用的构造函数;

第三个实例,使用了std::move,将左值类型转换成右值类型,调用右值构造;

现在对该类进行模板化改造:

#include <iostream>
#include <string>
#include <utility>
#include <type_traits>

class Person {
private:
	std::string _name;
public:
	template<typename T>
	Person(T&& name)
		:_name(std::forward<decltype(name)>(name))
	{
		std::cout << "Person constructor(right_ref) function std::string." << std::endl;
	}


	Person(const Person& p) 
		:_name(p._name)
	{
		std::cout << "Person(const Person&) left_ref constructor." << std::endl;
	}

	Person(Person&& p) noexcept
		:_name(std::move(p._name))
	{
		std::cout << "Person(const Person&) right_ref constructor." << std::endl;
	}


	~Person() {
		std::cout << "~Person()" << std::endl;
	}
};

int main(void) {
	Person p(std::string("a"));
	Person copy_p(p);
	Person move_p(std::move(p));

	return 0;
}

这里删除了类的左值引用,并声明其右值引用为函数模板,当我们再次调用时,line-38这个拷贝构造函数会报错,给出的理由是“相较于编译期默认给出的左值拷贝构造函数,模板右值引用构造函数更为匹配,所以发生了函数调用错误”;

enable_if<>用于禁用模板的生成

enable_if的使用

为了解决这个问题,引入了一个辅助模板enable_if,用以对是否生成一个模板实例进行条件判断:

#include <iostream>
#include <type_traits>

template<typename T>
std::enable_if<std::is_same<T, int>::value>::type func() {
	std::cout << "success" << std::endl;
}

int main(void) {
	func<int>();

	return 0;
}

这里进行了一个条件判断,只有当给定的类型为int时,才会初始化一个func函数实体,从而实现对模板生成的限制;

enable_if用于解决左值构造函数报错

对于之前的因函数匹配的问题而导致的函数崩溃,我们可以使用enable_if对其进行修改:

template<typename T, typename = std::enable_if<std::is_convertible<T, std::string>::value>::type>
Person(T&& name)
	:_name(std::forward<decltype(name)>(name))
{
	std::cout << "Person constructor(right_ref) function std::string." << std::endl;
}

对右值引用构造模板进行改造,加入生成实例条件,这里引入了一个标准类型特征——std::convertible,

用于判断两个类型是否可以进行转换;

在此处判断了T类型是否可以转换成std::string类型,并依此作为是否生成一个模板实例的标准;

如此,便可以避免因参数的匹配问题而导致的错误;

使用概念简化enable_if的表达式

在C++20中,引入了新特性——概念,用于约束模板的生成;

 其基本语法为:

#include <iostream>
#include <concepts>

template<typename T>
requires std::is_same<int, T>::value
void func() {
	std::cout << "success" << std::endl;
}

int main(void) {
	func<int>();
	func<double>();
	return 0;
}

这里使用了概念的关键字requires,用于实现和enable_if类似的模板生成条件判断操作;

所以,我们可以通过使用C++20的新特性——概念,对enable_if进行表达式的优化;

相较于enable_if来说,requires关键字有以下优点:

  1. 语法更简洁清晰requires表达式的语法更加直观和简洁,它直接放在函数模板或者类模板的定义之后,使得约束条件更加直观。
  2. 直接支持多个约束requires表达式可以直接列出多个约束条件,而不需要额外的模板参数。这使得代码更加简洁和易于理解。
  3. 更好的错误消息:使用requires表达式的代码在编译期间出现错误时,通常会产生更加清晰和直观的错误消息,因为错误通常与约束条件直接相关。
  4. 可读性更高requires表达式将约束条件直接放在模板声明的位置,使得模板的约束条件更容易理解和维护。
  5. 不再需要额外的模板参数:相较于enable_ifrequires表达式不再需要额外的模板参数来激活或禁用模板的实例化,从而使得模板的使用更加简单。