(Effective Modern C++) – 第五章

Item 23: Understand std::move and std::forward.

变量即使他绑定的是个右值,他本身是一个左值的类型。

std::move无条件地将表达式转换为T&&。而std::forward不会更改表达式地类型(保持左右值地属性)转发给其他的参数。如果std::move(exp)地表达式exp是一个常量类型,那么const的限定是不会发生改变的。

Things to Remember
• std::move performs an unconditional cast to an rvalue. In and of itself, it
doesn’t move anything.
• std::forward casts its argument to an rvalue only if that argument is bound
to an rvalue.
• Neither std::move nor std::forward do anything at runtime.

Item 24: Distinguish universal references from rvalue references.

发生了类型推导的T&&、auto&&(lambda表达式使用)表示的是一个通用的引用形式。而没有发生类型推导的类型或者不完全是形式不是T&&则就是单纯的右值引用:

#include <iostream>
#include <vector>
template<typename T>
void f(std::vector<T> &&r) {//单纯的右值引用

}
int main()
{
	std::vector<int> d;
	f(std::move(d));
	return 0;
}
Things to Remember
• If a function template parameter has type T&& for a deduced type T, or if an
object is declared using auto&&, the parameter or object is a universal reference.
• If the form of the type declaration isn’t precisely type&&, or if type deduction
does not occur, type&& denotes an rvalue reference.
• Universal references correspond to rvalue references if they’re initialized with rvalues. They correspond to lvalue references if they’re initialized with lvalues.

Item 25: Use std::move on rvalue references, std::forward on universal references.

T&& rhs。rhs本身是一个左值而它所绑定的对象是右值。使用std::move就可以将rhs本身传递给其他的可以接受右值引用的参数。而通用引用由于左右值引用的不确定,使用std::forward<T>可以将参数(参数包)转发给其他的参数。不能把通用的引用通过std::move转换成右值引用:

void setName(T&& newName)  // newName是个通用引用
{ name = std::move(newName); }  // 可以编译,不过太糟了!太糟了!

调用setName的参数(以一个局部变量的引用传递)被无条件的转换为了右值引用,而移动赋值运算符可能会修改源对象的内容。所以当函数调用结束会后,原参数的值就是不确定的。

使用std::forward+参数转发可以避免使用两个函数分别处理T&&和const T&作为参数而导致的扩展性下降以及带来的多余拷贝的操作。

一般情况,编译器在遇到了函数内return一个局部变量或者是类的构造函数会进行RVO和NRVO优化。

  • 这两个优化只有在返回类型与return表达式返回的类型一致才行。
  • C++标准的规定

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

  • 同时return语句不能有if语句:因为RVO和NRVO都是使用主调函数的栈帧区域或者是另一块任意的区域来避免拷贝的。例如:
#include <iostream>
#include <vector>

class Number {
private:
	int *_data;
public:
	Number() :_data(new int) { std::cout << "Number::Number()" << std::endl; }
	Number(int u):_data(new int(u)){ std::cout << "Number::Number(int)" << std::endl; }
	Number(const Number& n){
		if (n._data) {
			_data = new int(*n._data);
		}
		else {
			_data = nullptr;
		}
		std::cout << "Number::Number(const Number&)" << std::endl;
	}
	//使用移动构造就可以避免拷贝
	//Number(Number && n) :_data(n._data) {
	//	n._data = nullptr;
	//	std::cout << "Number::Number(Number &&)" << std::endl;
	//}
	~Number() {
		delete _data;
		_data = nullptr;
		std::cout << "Number::~Number()" << std::endl;
	}
	Number &operator=(const Number &rhs) {
		if (this != &rhs)
		{
			delete _data;
			_data = new int(*rhs._data);
		}
		std::cout << "Number::operator=(const Number&)" << std::endl;
		return *this;
	}
	Number &operator=(Number &&rhs) {
		if (this != &rhs)
		{
			_data = rhs._data;
			rhs._data = nullptr;
		}
		std::cout << "Number::operator=(Number&&)" << std::endl;
		return *this;
	}
	void print_data_address() {
		std::cout << _data << std::endl;
	}
	//操作符
	Number operator+(const Number &rhs) {
		std::cout << "Number::operator+(const Number&)" << std::endl;
		Number n(*_data + *rhs._data);
		n.print_data_address();
		//return std::move(n);
		if (*_data > 3)
		{
			return n;
		}
		else
		{
			return Number();
		}
	}
};
int main()
{
	Number n1(5);
	Number n2(6);
	Number n3 = n1 + n2;
	n3.print_data_address();//n3和operator+的临时的对象的分配的地址相同
	return 0;
}

输出是:发生了新的对象构造(注意必须没有move构造函数,因为返回的表达式是一个临时对象,使用移动构造就可以避免拷贝)

Number::Number(int)
Number::Number(int)
Number::operator+(const Number&)
Number::Number(int)
0132E460
Number::Number(const Number&)
Number::~Number()
0132E3A0

当是没有if语句的时候,RVO或NRVO会自动开启,即使没有移动构造函数也是。

当返回的表达式是return std::move(n)时,返回的类型时Number&&与函数签名的返回类型不同,RVO和NRVO被禁用,所以会发生移动构造(此时移动构造可见):

输出:

Number::Number(int)
Number::Number(int)
Number::operator+(const Number&)
Number::Number(int)
0149A3D8
Number::Number(Number &&)
Number::~Number()
0149A3D8

copy返回值<使用move构造返回的速度<使用RVO或NRVO优化。所以如果能用RVO或NRVO优化的函数还是尽量用他。move返回表示的是返回一个局部对象的引用,这样的行为是未定义,例如+操作改为

……
Number &&operator+(const Number &rhs) {
		std::cout << "Number::operator+(const Number&)" << std::endl;
		Number n(*_data + *rhs._data);
		n.print_data_address();
		return std::move(n);
		//return n;
		//return Number();
	}
……

RVO会开启。输出:

Number::Number(int)
Number::Number(int)
Number::operator+(const Number&)
Number::Number(int)
0007B0D8
Number::~Number()
Number::Number(Number &&)
CCCCCCCC

n3自然就是那个局部变量。

总结:函数内返回的对象,编译器一定视这个对象为右值,编译期要么通过RVO或NRVO要么隐式地调用move构造函数来避免拷贝。而且移动构造函数必须存在,否则函数就不能返回局部对象!

Things to Remember
• Apply std::move to rvalue references and std::forward to universal refer‐
ences the last time each is used.
• Do the same thing for rvalue references and universal references being
returned from functions that return by value.
• Never apply std::move or std::forward to local objects if they would other‐
wise be eligible for the return value optimization.

Item 26: Avoid overloading on universal references.

重载使用通用引用的函数(通常他把参数转发给另一个函数),可能会造成转发的参数并不能被接受。例如在继承体系下:

#include<iostream>
#include <string>
//继承体系下的问题
class Person1 {
private:
	std::string _name;
public:
	template<typename T>
	Person1(T &&n):_name(std::forward<T>(n)){}
};
class SpecialPerson1 :public Person1 {
private:
public:
	template<typename T>
	SpecialPerson1(T &&n) :Person1(std::forward<T>(n)) {}
};
int main() {
	SpecialPerson1 s1("rtger");
	SpecialPerson1 s2(s1);
}

派生类的构造函数与转发到基类的构造函数的参数(用于基类的string成员的构造)有着不同:前者不能构造出一个string对象。

const SpecialPerson1 s1("rtger");
SpecialPerson1 s2(s1);这样可以确保能够调用最佳的匹配(编译器生成的拷贝构造函数)

Item27 会介绍重载T&&的方法:

Things to Remember
• Overloading on universal references almost always leads to the universal refer‐
ence overload being called more frequently than expected.
• Perfect-forwarding constructors are especially problematic, because they’re
typically better matches than copy constructors for non-const lvalues, and
they can hijack derived class calls to base class copy and move constructors.

Item 27: Familiarize yourself with alternatives to overloading on universal references.

有几个方法可以重载通用引用:

  • Pass by value(在上面的Person1类中,直接使用std::string 对象构造string成员,但会造成一些拷贝)
  • Use Tag dispatch

将函数的调用分离,使用type traits技术可以在编译的时候完成函数的匹配。

#include <iostream>
#include <vector>
#include <map>
#include <string>
std::vector<std::string> names;

std::string &getNameByID(int id) {
	return names[id];
}
template<typename T>
void addName(T&& n) {
	addNameImpl(std::forward<T>(n), std::is_integral<typename std::remove_reference<T>::type>());
}

template<typename T>
void addNameImpl(T &&n, std::false_type) {
	names.emplace_back(std::forward<T>(n));
}
void addNameImpl(int index, std::true_type) {
	names.emplace_back(getNameByID(index));
}

std::is_intergral<T>是一个type traits用来判断类型T是否是整型,如果是返回一个std::true_type类似于一个bool类型true的字面值类型。否则为std::false_type。这样就可以通过检查参数的类型来将参数转发给合适的处理函数。

  • Constraining templates that take universal references

std::enable_if可以在模板被实例化的时候,根据传递参数来进行重载函数的选择。std::enable_if<exp,T>,表示如果exp为true则type成员=T。否则编译出错。它的一个可能实现:(cppreference)

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

SFIINAE(Substitution failure is not an error)。这个技术可以实现enable_if。SFINAE有两个关键failure和error。比如有一堆候选函数,如果没有任何一个给定的参数不能匹配或者转换为形参,编译器并不会产生编译错误,而是将这个候选函数从候选函数列表中去除,寻找其他的版本。所以没有被匹配的函数可以被认为是Failure;而如果没有任何一个函数能匹配调用,则编译出错(Error)。

template <typename T>
void inc_counter(T& intTypeCounter, std::decay_t<decltype(++intTypeCounter)>* = nullptr) {
	++intTypeCounter;
}

template <typename T>
void inc_counter(T& counterObj, std::decay_t<decltype(counterObj.increase())>* = nullptr) {
	counterObj.increase();
}

void fff(int = 5) {

}
void doSomething() {
	Counter cntObj;
	uint32_t cntUI32;
	
	// blah blah blah
	inc_counter(cntObj);
	inc_counter(cntUI32);
}

两种类型的增加计数的方法(使用可以++的类型以及Counter类型)需要被同意为函数inc_counter。std::decay_t<decltype(counterObj.increase())>* = nullptr和std::decay_t<decltype(++intTypeCounter)>* = nullptr)(C++14)可以通过验证参数类型决定使用哪一个重载版本。

  • std::decay<T>可以将T还原为基本的类型或者是指针(为type成员):

1.若T指向“U的数组”或“到U的数组的引用”类型,则成员typedef type为U*。
2.否则,若T为函数类型F或到它的引用,则成员typedef type是std::add_pointer<F>::type。
3.否则,成员typedef type为std::remove_cv<std::remove_reference<T>::type>::type。

  • decltype不会计算值。

继承体系下:

#include <iostream>
#include <vector>
#include <map>
#include <string>
std::vector<std::string> names;

std::string &getNameByID(int id) {
	return names[id];
}
template<typename T>
void addName(T&& n) {
	addNameImpl(std::forward<T>(n), std::is_integral<typename std::remove_reference<T>::type>());
}

template<typename T>
void addNameImpl(T &&n, std::false_type) {
	names.emplace_back(std::forward<T>(n));
}
void addNameImpl(int index, std::true_type) {
	names.emplace_back(getNameByID(index));
}
//继承体系下的问题
class Person {
private:
	std::string _name;
public:
	template<
		typename T,
		typename = typename std::enable_if<
		!std::is_base_of<Person,
		typename std::decay<T>::type
		>::value
		&&
		!std::is_integral<std::remove_reference_t<T>>::value
		>::type
	> explicit Person(T &&n):_name(std::forward<T>(n)) {
		//检查参数是否匹配
		static_assert(
			std::is_constructible<std::string, T>::value,
			"Parameter n can't be used to construct a std::string"
			);
	}
	//重载版本 当T为整型&
	explicit Person(int n):_name(getNameByID(n)) {

	}
};
class SpecialPerson :public Person {
private:
public:
	template<typename T>
	SpecialPerson(T &&n):Person(std::forward<T>(n)){}

	SpecialPerson(const SpecialPerson &p):Person(p) {

	}
};

Person基类的类型有两个重载的构造函数:

  1. 参数是否是从Person派生的(一定要使用decay,因为Derived&不是Base的派生类。用于派生类初始化基类)且参数不是整型。第一个匹配,否则第二个
  2. 如果第一个不匹配,使用第二个用于显式从int“构造”出string对象。
  • 转发的问题(Trades-off)

1.如果实参本身不能匹配被转发到的函数,则会发生无法转换类型的错误。例如:

Person p(u"Konrad Zuse");   // "Konrad Zuse" consists of
                            // characters of type const char16_t

string并没有定义接受char16_t[12]的构造函数

2.使用static_assert(exp,message),可以把exp==false时候的信息message报告给编译器。这样就可以在有许多模板实例化错误之后给出一个合理的信息(因为模板的调用有时候传递了几层给实参的接受方,一个错误可能会导致很难以理解的问题)

 static_assert(
      std::is_constructible<std::string, T>::value,
      "Parameter n can't be used to construct a std::string"
Things to Remember
• Alternatives to the combination of universal references and overloading
include the use of distinct function names, passing parameters by lvalue-
reference-to-const, passing parameters by value, and using tag dispatch.
• Constraining templates via std::enable_if permits the use of universal ref‐
erences and overloading together, but it controls the conditions under which
compilers may use the universal reference overloads.
• Universal reference parameters often have efficiency advantages, but they typ‐
ically have usability disadvantages.

Item 28: Understand reference collapsing.

引用的引用在C++中并不合法。所以C++通过引用折叠计数来避免使用引用的引用。例如一个forward的实现:

template<typename T>                                // in
T&& forward(typename                                // namespace
              remove_reference<T>::type& param)     // std
{
  return static_cast<T&&>(param);
}

当传入的是左值的。T被推导为T&,返回的就是左值引用。如果传入的是右值,那么T被推导为T,静态转换的就是右值。

注意只有在需要进行类型推导的情况下,通用引用才可以发生引用折叠。

typedef的问题:

typedef T&& RvalueRefToT;

由于发生引用折叠,所以RvalueRefToT可能与表面的意思并不相同(可能是一个左值引用类型)

Things to Remember
• Reference collapsing occurs in four contexts: template instantiation, auto type
generation, creation and use of typedefs and alias declarations, and
decltype.
• When compilers generate a reference to a reference in a reference collapsing
context, the result becomes a single reference. If either of the original refer‐
ences is an lvalue reference, the result is an lvalue reference. Otherwise it’s an
rvalue reference.
• Universal references are rvalue references in contexts where type deduction
distinguishes lvalues from rvalues and where reference collapsing occurs.

Item 29: Assume that move operations are not present, not cheap, and not used.

  • 有的时候,move并不一定比copy快很多。例如容器array,他将分配一个固定的区域用于保存元素。所以copy和move都将对已有的元素进行copy和move操作。这都是耗费线性的时间。
  • 如果类内部使用指针指向堆上的区域,那么move比copy快就是有可能的。例如string,他就有可能。但是对于小的string,类会使用small string optimization(SSO)的优化(<=15个字符保存在char[15]中)。这样的话,move并不一定比copy快。

为了确保C++98的旧代码提升为C++11的时候,提供了noexcept保证的时候,才会把copy当作move使用。只有当move操作有noexcept保证时,编译期才有可能选择move(即使操作的对象时T&&)

移动操作不好的地方:

  • 移动操作被拒绝:移动变为拷贝
  • 移动不一定比copy快
  • 有些上下文情况下,移动操作必须做出noexcept保证。
  • 除去极少的情况(Item25),move操作的对象必须是右值引用
Things to Remember
• Assume that move operations are not present, not cheap, and not used.
• In code with known types or support for move semantics, there is no need for
assumptions.

如果明确知道类的move语义。就不需要做出“not present, not cheap, and not used”的假设。如果不能明确(或者类的代码频繁改动),还是使用拷贝而非移动操作为好。

Item 30: Familiarize yourself with perfect forwarding failure cases.

完美转发的目的就是间接的将与传入参数类型相同的参数(保存cv限定符、左右值等)转发给另一个函数。这个规则排除:

  • 使用值传递
  • 指针类型的传递,转发指针的引用

假设函数:

template<typename... Ts>
void fwd(T&& ...params)   //  接受一些参数
{
    f(std::forward<Ts>(param)...);    // 把它们转发到f
}

定义表达式:

f( expression );       // 如果f做某件事
fwd(expression);       // 而fwd里的f做的是不同的事,那么fwd完美转发expression失败

完美转发失败的情况:

  • 大括号初始值

f({1,2,3})是没有问题的,因为{...}可以隐式转换为std::vector<int>对象

当下面的情况出现时,转发会失败:

  • 编译器不能推断出传递给fwd参数的类型,在这种情况下,代码编译失败。
  • 编译器为传递给fwd参数推断出“错误的”类型。这里的“错误的”,可以表示实例化的fwd不能编译推断出来的类型,还可以表示使用fwd推断的类型调用f的行为与直接使用传递给fwd的参数调用f的行为不一致。这种不一致行为的一种源头是f是个重载函数的名字,然后,根据“不正确的”推断类型,fwd里的f重载调用与直接调用f的行为不一样。

在上面“fwd({1,2,3})”的例子中,问题在于,给没有声明为std::initializer_list类型的模板参数传递大括号初始值,正如标准库所说,这是“non-deduced context”。通俗易懂的说,那意味着在fwd的调用中,编译器禁止从表达式{1,2,3}推断出一个类型,因为fwd的形参不是声明为std::initializer_list。因为那是被禁止推断,所以编译器拒绝这样调用。

解决的方式:

//1
auto il = { 1, 2, 3 };     // il's type deduced to be
                           // std::initializer_list<int>
fwd(il);                   // fine, perfect-forwards il to f
//2
fwd<std::initializer_list<int>>({ 1,2 });
  • 0和NULL作为空指针

0、NULL是Int和int的宏,他们会作为整型字面值常量被转发。使用nullptr可以作为指针类型转发。

  • 只声明的static const成员变量

有的编译器静态常量成员需要定义初始值。类内仅仅是声明而不是定义。所以需要在实现的文件中定义这个成员。

#include <utility>
class A{
    public:
    static const int tv=25;
};
// const int tv=25; //comment 1

void f(int)
{

}
template<typename... T>
void fwd(T&&... t)
{
    f(std::forward<T>(t)...);
}
int main(){
    f(A::tv);
    fwd(A::tv);
    return 0;
}

当comment1注释没有去除的时候,fwd的调用会发生链接错误,而f没有。因为使用引用(其实在很多方面与指针差不多。要使用指针必须能够取得变量的地址。而静态的成员是所有实例化的对象共有的。还有一个事实:编译器不会给静态的常量成员分配内存空间)。虽然编译的时候看上去没有地址的引用,但是在链接的时候就会有。如果f的定义改为:

void f(const int&l)。同样链接也会出错。

解决方案:在实现文件中定义成员

const std::size_t Widget::MinVals; // 注意不要重复初始值

  • 重载函数名字和模板名字

函数的模板代表的是多个函数。假设下面的代码。pf1是一组重载的函数,

#include <iostream>
#include <utility>
void f(void(*pf)(int))
{
}

template <typename... T>
void fwd(T &&... para)
{
	f(std::forward<T>(para)...);
}
void pf1(int t) {

}
void pf1(int th, int t) {

}
int main()
{
	f(pf1);
	fwd(pf1);//error
	return 0;
}

在调用f(pf1)中,pf1既不是函数指针也不是函数而是两个pf1函数的名称。但是编译器可以决定哪一个函数适合f的参数。

在调用fwd(pf1)中,pf1是重载函数,并不包含类型的信息,所以模板无法推导。

类似的,如果pf1是模板函数,它可以代表多个函数(根据模板参数的不同),所以这也不包含类型的信息

解决的办法:手动确定转发哪一个重载/模板的函数类型

  1. fwd(static_cast<void(*)(int)>(pf1));
  2. using ProcessFuncType = int(*)(int);
    
    ProcessFuncType processValPtr = pf1;    // 指定了pf1的签名
    
    fwd(processValPtr );          // 正确
  • 位域(Bitfields)

设一个类型

struct IPv4Header {
  std::uint32_t version:4,
                IHL:4,
                DSCP:6,
                ECN:2,
                totalLength:16;
  …
};

有一点需要记住:非常量的引用不能绑定到位域。位域可能是包括机器字(world)的任意部分(例如,32位int的3-5个位。),但是没有方法直接获取它们的地址。我之前提起过在硬件层面上引用和指针是相同的东西,然后,就像没有办法创建指向任意位的指针(C++表明可指向的最小的东西是一个char),也没有办法对任意位进行绑定引用。

void f(std::size_t sz);   // 被调用的函数

IPv4Header h;
...
f(h.totalLength);         // 正确
//但是,想要借助fwd把h.totalLength转发f,就是另外的结果了:

fwd(t.totalLength);      // 错误

同时,由于位域是不规则的,指针无法绑定自然引用也不会存在。所以位域一般是使用值传递的。或者是引用绑定到位域的一个副本(一般将位域转换为一个标准的类型)。

//  拷贝位域的值,初始化形式看条款6
auto length = static_cast<std::uint16_t>(h.totalLength);

fwd(length);   // 转发拷贝

总结:

大多数情况下,完美转发工作正常。但有时会失败,可能是编译时也可能是链接的时候或者是不正常地工作。

Things to Remember
• Perfect forwarding fails when template type deduction fails or when it deduces
the wrong type.
• The kinds of arguments that lead to perfect forwarding failure are braced ini‐
tializers, null pointers expressed as 0 or NULL, declaration-only integral const
static data members, template and overloaded function names, and bitfields.

引用&参考