一般情况下,建议函数模板中按值传递参数,除非有必须要用引用传递的理由。
需要使用引用传递的几种情况:
- 对象不可复制:
在C++17之前,如果一个类类型没有定义复制构造函数或移动构造函数,那么即使尝试以值传递的方式传递一个临时对象(右值),也会导致编译错误,因为编译器无法找到合适的方式来创建新对象。然而,在C++17中引入了“guaranteed copy elision”(保证拷贝消除)的概念,即使没有显式定义复制或移动构造函数,编译器在某些情况下也能安全地省略临时对象的创建和后续的复制或移动操作。这就意味着在C++17及以后的标准中,可以更高效地通过值传递临时对象,即使没有可用的复制或移动构造函数。
-
参数用于返回数据
减少了参数传递和返回值所带来的拷贝开销 -
模板只是通过保留原始参数的所有属性,将参数转发到其他地方
使用万能引用(T&&) + 完美转发,保证参数保留原始类型 -
有显著性能改进
如果这里是系统的性能瓶颈,可以考虑采用引用传递的方式来进行优化。
值类别
首先了解一下值类别的概念:
lvalue(左值):表示一个可寻址的、持续存在的对象,它的地址可以被赋值给一个左值引用。通常情况下,左值是指在程序中具有稳定位置的对象,例如全局变量、局部变量等。
rvalue(右值):表示一个临时对象或纯右值,它通常是一个表达式的结果,不具有稳定的位置,生命周期较短。例如,字面量(如数字、字符等)和函数调用返回值都是右值。
prvalue(纯右值):是最严格的右值类型,表示一个不会在内存中占用位置的临时对象,通常是没有命名的。例如,字面量、函数调用返回值以及使用某些运算符(如“+”、“-”等)的结果都是纯右值。
xvalue(将亡值):也称为“将要消亡的值”,表示一个即将被销毁的对象,通常是一个临时对象。与prvalue不同的是,xvalue可以被绑定到一个右值引用上,从而延长其生命周期。例如,使用std::move()函数返回的对象就是xvalue。
按值传递
按值传递参数时,原则上必须复制每个参数,每个参数都成为所传递实参的副本, 但编译器通 常会优化传递参数,这样就不会调用复制构造函数了。C++17 起,这种优化是必需的。C++17 前, 不能优化复制的编译器,至少需要使用移动语义,这通常会使复制成本降低。
std::string returnString();
std::string s = "hi";
printV(s); // copy constructor
printV(std::string("hi")); // copying usually optimized away (if not, move constructor)
printV(returnString()); // copying usually optimized away (if not, move constructor)
printV(std::move(s)); // move constructor
值传递还有一个性质:类型衰变,比如传入的是const char[*], 但是在经过模板函数传参后衰变成了const char*, 并删除 const 和 volatile 等限定符 (就像使用值作为使用 auto 声明的对象的初始化式一样)
#include <iostream>
// 模板函数,用于展示类型衰变现象
template<typename T>
void Print(T value) {
// 数组衰变(退化)成了指针
std::cout << "Decayed type: " << typeid(value).name() << '\n';
}
int main() {
// 示例字符串常量
const char str[] = "Hello, World!";
// 传入数组时,模板函数参数的实际类型将会发生类型衰变
Print(str);
Print("name"); // 传入字面量,也依然被会衰变成指针
return 0;
}
按引用传递
为了避免不必要的复制,在传递临时对象时,可以使用常量引用(const T&)传递
#include <iostream>
template<typename T>
void Print(const T& value) {
// 传递引用类型不会衰退
std::cout << "Decayed type: " << typeid(value).name() << '\n';
}
int main() {
// 示例字符串常量
const char str[] = "Hello, World!";
Print(str); // type: A14_c 表示一个大小为14的字符数组
Print("name"); // type: A5_c 表示一个大小为5的字符数组
int a = 30;
Print(a); // type: i 表示一个整数
Print(&a); // type: Pi 表示一个整数指针
const int* p = &a;
Print(p); // type: PKi 表示一个带const的整数指针
return 0;
}
// 不带const的引用传递,不允许传递临时变量或者std::move后的变量
template<typename T>
void Print(T& value) {
// 传递引用类型不会衰退
std::cout << "Decayed type: " << typeid(value).name() << '\n';
}
int main() {
// 示例字符串常量
const char str[] = "Hello, World!";
Print(str); // type: A14_c 表示一个大小为14的字符数组
std::string ss = "name";
Print(ss); // type: NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
// Print(std::move(ss)); // 编译失败,无法传递std::string 变量的右值引用
// Print(std::string("name")); // 编译失败,无法传递std::string的临时对象
const std::string const_ss = "abcd"; // 常量字符串,则可以使用std::move() ...
Print(const_ss); // type: NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
Print(std::move(const_ss)); // NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
int a = 30;
Print(a); // type: i 表示一个整数
// Print(int{6}); //编译失败,无法传递std::string的临时对象
return 0;
}
使用 std::ref() 和 std::cref()
C++11 起,可以让调用者决定函数模板参数是通过值传递,还是通过引用传递。当模板声明为 按值接受参数时,调用者可以使用在头文件 中声明的 std::cref() 和 std::ref(),通过引用 传递参数。
#include <iostream>
#include <functional>
template<typename T>
void Print(T value) {
std::cout << "Decayed type: " << typeid(value).name() << '\n';
}
int main() {
int a = 3;
Print(a); // 值传递
Print(std::ref(a)); // 显式的引用传递
Print(std::cref(a)); // 显式的const引用传递
return 0;
}
std::ref 和 std::cref 本质上没有改变函数模板的展开逻辑,而是创建了一个 std::reference_wrapper<> 对象,然后按值传递了该对象到模板函数中。
#include <functional>
#include <iostream>
#include <string>
void printString(const std::string& str) {
std::cout << str << std::endl;
}
template<typename T>
void Print(T value) {
printString(value);
}
int main() {
std::string str = "hello";
Print(str); // 值传递
Print(std::cref(str)); // 引用传递
return 0;
}
编译器必须知道返回原始类型必要的隐式转换。因此,只有通过泛型代码将对象传递给非泛型 函数时,std::ref() 和 std::cref() 才能正常工作。所以一种错误用法为:
template<typename T>
void Print(T value) {
std::cout<<value<<std::endl;
}
int main() {
std::string str = "hello";
Print(str); // OK
// Print(std::cref(str)); // 错误, std::reference_wrapper<> 没有定义<<操作符(因为无法推导),所以这种用法是错误的
return 0;
}
处理字符串字面值和数组
模板参数在使用字符串字面值和数组时会产生不同的效果:
- 按值调用会衰变,使其成为指向元素类型的指针。
- 引用调用都不会衰变,因此参数成为仍然是数组
处理返回值
函数模板的返回值,可以是通过值返回,还可以通过引用返回
通常推荐值返回,避免不必要的麻烦,但有些场景下,更推荐引用返回:
- 返回容器或字符串元素 (例如,通过 operator[] 或 front())
- 授予类成员写访问权限
- 返回链式调用的对象 (流的 operator<< 和 operator>>,类对象的 operator=)
此外,通过返回 const 引用来授予成员读权限。
使用引用返回务必小心,对于对象的生命周期要把我清楚,否则很容易出错:
#include <iostream>
#include <string>
template<typename T>
T& func(T& t) {
return t;
}
int main() {
std::string* str_ptr = new std::string{"hello"}; // 创建一个字符串指针,指向一个字符串对象
std::string& str = func(*str_ptr);
std::cout<<str<<std::endl; // 对象没有释放前访问,OK
delete str_ptr;
std::cout << str << std::endl; // ERROR : str_ptr 已经被释放了,造成引用悬垂
return 0;
}
即使 T 是由按值调用推导而来的模板参数,当显式指定模板参数为引用时,也可能成为引用类 型:
#include <iostream>
#include <string>
template<typename T>
T& func(T t) {
return t;
}
int main() {
std::string* str_ptr = new std::string{"hello"}; // 创建一个字符串指针,指向一个字符串对象
std::string& str = func<std::string&>(*str_ptr); // 显式传递引用
std::cout<<str<<std::endl; // 对象没有释放前访问,OK
delete str_ptr;
std::cout << str << std::endl; // ERROR : str_ptr 已经被释放了
return 0;
}
安全实践:
- 使用值返回,并且在函数模板中显式消除引用,防止意料之外的类型推导:
template<typename T>
typename std::remove_reference<T>::type retV(T p) {
return T{...}; // always returns by value }
- 编译器通过声明返回类型 auto 来推断返回类型 (C++14 起),因为 auto 总会衰
变
template<typename T>
auto retV(T p) // by-value return type deduced by compiler {
return T{...}; // always returns by value }
类型特征 std::decay<> 允许在引用传递的模板中显式地衰变参数。