通用引用版本和右值引用版本的一些异同比较
假定要实现一个person类,并且实现一个设置person name的方法setName。
第一种,通用引用版本(对通用引用使用std::forward)
class person
{
public:
template<class T>
void setName(T&& name)
{
name_ = std::forward<T>(name);
}
private:
std::string name_;
};
第二种,重载左值引用和右值引用版本
class person
{
public:
void setName(const std::string& name)
{
name_ = name;
}
void setName(std::string&& name)
{
name_ = std::move(name);
}
private:
std::string name_;
};
(1)非const左值引用只能绑定到非const左值;
(2)const左值引用可绑定到const左值、非const左值、const右值、非const右值;
(3)非const右值引用只能绑定到非const右值;
(4)const右值引用可绑定到const右值和非const右值。
两种版本比较:
-
万能引用版本传入参数不能是const。如果传入的name参数是不可更改的,const左值引用版本会更符合。
更正:以上理解错误。根据模板类型推导,万能引用版本如果传入const左值引用,T和ParamType都会被推到为const左值引用类型。无论是const左值引用还是非const左值引用传入后都为const左值引用类型。而万能引用版本非const左值引用传入参数还是非const左值引用。所以重载左值版本只是为了将传入的参数加上const属性限制。 -
考虑一种使用场景:
person p;
p.setName("abc");
如果是通用引用版本,字符串"abc"是一个右值,T会被推导为const char*
类型,parmtype推导为const char* &&
类型,直接将字符串"abc"传入函数,调用std::string的赋值操作,期间没有产生std::string的临时对象。
如果是重载右值引用的版本,字符串"abc"会首先构造一个临时string对象传入,然后调用string的移动赋值操作(右值版本的赋值操作符)。最后还有一次临时string的对象析构。这样的开销肯定要比通用引用版本的开销要大。
c++11 stl中容器新增的emplace_方法其实就是用的万能引用版本。
例如vector的emplace_back方法:
template <class... Args>
void emplace_back (Args&&... args);
我们大概可以猜测emplace_back内部也应该用了forward方法。
谨慎地对通用引用构造函数进行重载
class person
{
public:
template<class T>
person(T&& t) : name_(std::forward<T>(t)) {}
person(const person& p) {} // 编译器自动生成的拷贝构造
person(person&& p) {} // 编译器自动生成的移动构造
template<class T>
void setName(T&& name)
{
name_ = std::forward<T>(name);
}
private:
std::string name_;
};
一个通用引用构造,编译期自动生成的拷贝构造和移动构造。
考虑下面这种用法,
person a("abc");
person b(a);
我们期望person b(a);
a是一个左值,这句代码会调用拷贝构造,但实际上会编译出错。
因为a是非const类型,优先匹配了万能引用的构造函数,T被识别为person&类型。又因为string没有参数类型为person&的构造函数类型,导致编译出错。
所以应该谨慎地对通用引用构造函数进行重载。
另外当通用引用和普通重载匹配度一样时,优先调用普通重载函数。
返回值语义
- 如果你有个函数是通过值返回,然后你函数内返回的是被右值引用或通用引用绑定的对象,那么你应该对你返回的对象使用std::move或std::forward。
class mystring
{
public:
mystring(const char* str) : str_(str)
{
std::cout << "mystring(const char* str)" << std::endl;
}
mystring(const mystring& ms)
: str_(ms.str_)
{
std::cout << "mystring(const mystring& ms)" << std::endl;
}
mystring(mystring&& ms)
: str_(std::move(ms.str_))
{
std::cout << "mystring(mystring&& ms)" << std::endl;
}
mystring& operator+=(const mystring rhs)
{
std::cout << "operator+=" << std::endl;
str_ += rhs.str_;
return *this;
}
private:
std::string str_;
};
mystring mystrcat1(mystring&& str1, const mystring& str2)
{
str1 += str2;
return str1;
}
mystring mystrcat2(mystring&& str1, const mystring& str2)
{
str1 += str2;
return std::move(str1);
}
比较mystrcat1和mystrcat2调用结果,
如果是不用std::move或者std::forward,会调用拷贝构造拷贝到返回区,而如果是用std::move或者std::forward,会调用移动构造移动到返回区。假如参数类型支持移动构造,它比拷贝构造效率更高,那么在返回语句中使用std::move会产生更高效的代码。如果参数类型不支持移动构造,把它转换为右值也是无伤害的,因为此时右值会简单地作为参数来调用拷贝构造函数。
stl中basic_string类的operator+函数就是这种用法。
template <class _Elem, class _Traits, class _Alloc>
_NODISCARD inline basic_string<_Elem, _Traits, _Alloc> operator+(basic_string<_Elem, _Traits, _Alloc>&& _Left,
_In_z_ const _Elem* const _Right) { // return string + NTCTS
return _STD move(_Left.append(_Right));
}
- 如果是函数内的局部变量,因为C++编译器都会进行RVO优化来避免拷贝,所以无需std::move或std::forward。对局部变量用了std::move或std::forward反而会阻碍RVO优化,得不偿失。
参考:https://blog.csdn.net/big_yellow_duck/article/details/52388820