《effective modern C++》Chapter 1-3
[TOC]
本文记录《effective modern C++》前 3 章的学习笔记.
CHAPTER 1 型别推导
前言
类型推导的历史:
- C++98 had a single set of rules for type deduction: the one for function templates.
- C++11 modifies that ruleset a bit and adds two more, one for
auto
and one fordecltype
. - C++14 then extends the usage contexts in which
auto
anddecltype
may be employed.
本章内容简介:
- It explains how template type deduction works,
- how
auto
builds on that, and - how
decltype
goes its own way. - It even explains how you can force compilers to make the results of their type deductions visible, thus enabling you to ensure that compilers are deducing the types you want them to.
Item 1: Understand template type deduction.
pseudocode of template function:
1 | template<typename T> |
compilers use expr
to deduce two types:
- one for
T
and - one for
ParamType
.
2 者经常不同:ParamType
often contains adornments, e.g.,const
or reference qualifiers.
例如, T
is deduced to be int
, but ParamType
is deduced to be const int&
.
1 | template<typename T> |
The type deduced for T
is dependent not just on the type of expr
, but also on the form of ParamType
. There are three cases:
Case 1: ParamType
is a Reference or Pointer, but not a Universal Reference
推导规则:
- If
expr
’s type is a reference, ignore the reference part. - Then pattern-match
expr
’s type againstParamType
to determineT
.
param
is a reference
1 | template<typename T> |
注意: because cx
and rx
designate const
values, T
is deduced to be const int
, thus yielding a parameter type of const int&
.
原因: 因为 const
对象本身就不能被修改, 因此把 const
对象是 pass-by-value 还是 pass-by-const
-reference 都没有差别, 因此可以出现 cx
传进去依然可以推导出 const int&
的情况.
机制: the const
ness of the object becomes part of the type deduced for T
. 也就是在引用的类型推导不改变 const
ness.
change the type of f
’s parameter from T&
to const T&
==> no longer a need for const
to be deduced as part of T
:
1 | template<typename T> |
param
is a pointer
1 | template<typename T> |
Case 2: ParamType
is a Universal Reference
If
expr
is an lvalue, bothT
andParamType
are deduced to be lvalue references.- First, it’s the only situation in template type deduction where
T
is deduced to be a reference. - Second, although
ParamType
is declared using the syntax for an rvalue reference, its deduced type is an lvalue reference.
- First, it’s the only situation in template type deduction where
If
expr
is an rvalue, the “normal” (i.e., Case 1) rules apply.
1 | template<typename T> |
Case 3: ParamType
is Neither a Pointer nor a Reference(pass-by-value)
推导规则:
- As before, if
expr
’s type is a reference, ignore the reference part. - If, after ignoring
expr
’s reference-ness,expr
isconst
orvolatile
, ignore that, too.
1 | template<typename T> |
注意: cx
与 rx
的 const
ness 被削减掉了.
原因: why expr
’s const
ness (and volatile
ness, if any) is ignored when deducing a type for param
: just because expr
can’t be modified doesn’t mean that a copy of it can’t be.
特殊情况: 指向 const
对象的 const
指针通过 pass-by-value 传入
1 | template<typename T> |
推导的结果: param
will be const char*
, i.e., a modifiable pointer to a const
character string.
Array Arguments
pass-by-value
数组自动转换为指针: an array decays into a pointer to its first element.
机制: an array cannot be a function parameter that’s. void myFunc(int param[]);
非法.
1 | const char name[] = "J. P. Briggs"; // name's type is const char[13] |
pass-by-reference
functions can declare parameters that are references to arrays.
原理: springing from the C roots at the base of C++.
1 | template<typename T> |
T
is deduced to be const char [13]
, and the type of f
’s parameter (a reference to this array) is const char (&)[13]
. 注意类型里包含数量.
the ability to declare references to arrays enables creation of a template that deduces the number of elements that an array contains, 推导出数组的元素数目:
1 | // return size of an array as a compile-time constant. (The |
用处: declaring this function constexpr
makes its result available during compilation.
1 | int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };//keyVals has 7 elements |
Function Arguments
Function types can decay into function pointers. Everything we’ve discussed regarding type deduction for arrays applies to type deduction for functions.
1 | void someFunc(int, double); // someFunc is a function; type is void(int, double) |
This rarely makes any difference in practice.
Things to Remember
- During template type deduction, arguments that are references are treated as non-references, i.e., their reference-ness is ignored.
- When deducing types for universal reference parameters, lvalue arguments get special treatment.
- When deducing types for by-value parameters, const and/or volatile arguments are treated as non-const and non-volatile.
- During template type deduction, arguments that are array or function names decay to pointers, unless they’re used to initialize references.
Item 2: Understand auto
type deduction.
with only one curious exception, auto
type deduction is template type deduction.
与模板推断相同的地方
There’s a direct mapping between template type deduction and auto type deduction. auto
推导与模板类型推导是同源的(essentially two sides of the same coin), with only one exception(braced initializers).
auto
plays the role of T
in the template, and the type specifier for the variable acts as ParamType
.
同样分为 3 类:
- Case 1: The type specifier is a pointer or reference, but not a universal reference.
- Case 2: The type specifier is a universal reference.
- Case 3: The type specifier is neither a pointer nor a reference.
1 | auto x = 27; // case 3 (x is neither ptr nor reference) |
array and function names decay into pointers for non-reference type specifiers:
1 | const char name[] = "R. N. Briggs"; // name's type is const char[13] |
与模板推断不同的地方
a special type deduction rule for auto
:
When the initializer for an auto
-declared variable is enclosed in braces, the deduced type is a std::initializer_list
.
1 | //C++98: |
但是 auto
对 std::initializer_list
的推断也不是无限层次的(自己的理解, 原文未提及), 例如:
1 | template<typename T> // template with parameter |
结论: So the only real difference between auto
and template type deduction is that auto
assumes that a braced initializer represents a std::initializer_list
, but template type deduction doesn’t.
没有什么合理的理由: 当成 rule 来记吧.
C++ 14 中的 auto
新增的功能:
- permits
auto
to indicate that a function’s return type should be deduced.
1 | auto createInitList() |
- lambdas may use
auto
in parameter declarations.
1 | std::vector<int> v; |
auto
的 2 个使用的意义: template type deduction 与 auto
type deduction, C++ 11 中的为后者, C++ 14 中增加了前者.
Things to Remember
auto
type deduction is usually the same as template type deduction, butauto
type deduction assumes that a braced initializer represents astd::initializer_list
, and template type deduction doesn’t.auto
in a function return type or a lambda parameter implies template type deduction, not auto type deduction.
Item 3: Understand decltype
.
decltype
in C++11
一般用法(最直觉)
decltype
typically parrots back the exact type of the name or expression(后面会讲这两者应用的区别) you give it:
1 | const int i = 0; // decltype(i) is const int |
trailing return type
C++11
1 | template<typename Container, typename Index> // works, but requires refinement |
auto
has nothing to do with type deduction. 仅仅只是 placeholder.
decltype(auto)
in C++14
C++11 permits return types for single-statement lambdas to be deduced, and C++14 extends this to both all lambdas and all functions, including those with multiple statements.
因此在 C++ 14 中可以直接用 auto
推导返回值类型即可. 但是这么做有个问题–auto
strips off the reference.
1 | template<typename Container, typename Index> // C++14; not quite correct |
无法编译的解释: auto
will strip off the reference, thus yielding a return type of int
(not a int &
). That int
, being the return value of a function, is an rvalue, and the code above thus attempts to assign 10 to an rvalue int
. ==> forbidden in C++, so the code won’t compile.
- 使用
decltype(auto)
1 | template<typename Container, typename Index> // C++14; works, but still requires refinement |
为什么会是这种形式 decltype(auto)
? <== makes perfect sense: auto
specifies that the type is to be deduced, and decltype
says that decltype
rules should be used during the deduction.
- apply
decltype(auto)
to initializing expression
不止是函数/lambda, decltype(auto)
也可以用在 expression 上.
1 | Widget w; |
refinement with respect to rvalue reference
1 | template<typename Container, typename Index> |
上面的参数是 container is passed by lvalue-reference-to-non-const
, 因此 not possible to pass rvalue containers to this function(unless they’re lvalue-references-to-const
). 如果说传入右值的 container 不是合理的, 那已经可以满足需求了, 然而存在合理使用右值引用的场景举例(一般而言不合理是因为返回一个对右值的引用/值可能会导致 dangling):
1 | std::deque<std::string> makeStringDeque(); // factory function |
虽然可以通过 overload 分别应用到左右值引用. 更推荐 universal reference
1 | template<typename Container, typename Index> // c is now a |
PS. 这里作者很严谨地进行了下面的探讨:
对于 Index i
参数作者虽然不推荐使用 pass-by-value, 但是考虑到 STL 中的下标一般都是 int
类似的, 可以使用 pass-by-value(当然也存在 std::map
的 key 是比较大的类型的情况).
加上完美转发 std::forward
:
1 | template<typename Container, typename Index> // final C++14 version |
对应的 C++11 版本:
1 | template<typename Container, typename Index> // final C++11 version |
对非 name 的 expression 使用 decltype
的风险
decltype
的对象是 name(更严格一点地说是 entity) 与 expression.
- 对于 name, applying
decltype
to a name yields the declared type for that name. Names are lvalue expressions, but that doesn’t affectdecltype
’s behavior. - 如果是非 name 的 expression:
decltype
ensures that the type reported is always an lvalue reference.
为什么是返回左值引用呢? because the type of most lvalue expressions inherently includes an lvalue reference qualifier. Functions returning lvalues, for example, always return lvalue references.
这里推荐 cppreference 上对 decltype
的正式说明.
例子如下:
1 | int x = 0; |
x
is the name of a variable, so decltype(x)
is int
.
C++ defines the expression (x)
to be an lvalue, decltype((x))
is therefore int&
.
风险举例:
1 | decltype(auto) f1() |
Things to Remember
decltype
almost always yields the type of a variable or expression without any modifications.- For lvalue expressions of type
T
other than names,decltype
always reports a type ofT&
. - C++14 supports
decltype(auto)
, which, likeauto
, deduces a type from its initializer, but it performs the type deduction using thedecltype
rules.
Item 4: Know how to view deduced types
three phares
IDE Editors
简单的推断.
For this to work, your code must be in a more or less compilable state.
when more complicated types are involved, the information displayed by IDEs may not be particularly helpful.
Compiler Diagnostics
通过故意生成错误让编译器显示出编译器推断的结果.
1 | template<typename T> // declaration only for TD; |
编译一定会报错: because there’s no template definition to instantiate.
一种报错输出:
1 | error: aggregate 'TD<int> xType' has incomplete type and cannot be defined |
可以看到 'TD<int> xType'
的部分把编译器推断的结果打印了出来.
Runtime Output
1 | template<typename T> // template function to be called |
GNU and Clang compilers produce this output:
1 | T = PK6Widget |
i
means int
and PK
means pointer to const
. number 6
: the number of characters in the class name that follows (Widget
).
Microsoft’s compiler concurs:
1 | T = class Widget const * |
有个明显的错误:
it seems odd that T
and param
have the same type. If T
were int
, for example, param
’s type should be const int&
—not the same type at all.
the results of std::type_info::name
are not reliable.
为什么会这样?
they’re essentially required to be incorrect, because the specification for std::type_info::name
mandates that the type be treated as if it had been passed to a template function as a by-value parameter.
That’s why param
’s type—which is const Widget * const &
—is reported as const Widget*
. First the type’s reference-ness is removed, and then the constness of the resulting pointer is eliminated.
所幸的是还有 Boost 的库可以帮助我们查看.
Boost TypeIndex library
放心食用: using Boost libraries is nearly as portable as code relying on the Standard Library.
1 |
|
- 语源
with_cvr
: it doesn’t removeconst
,volatile
, or reference qualifiers.
pretty_name
member function produces a std::string
containing a human-friendly representation of the type.
Boost TypeIndex 的输出:
1 | T = Widget const* |
Things to Remember
- Deduced types can often be seen using IDE editors, compiler error messages, and the Boost TypeIndex library.
- The results of some tools may be neither helpful nor accurate, so an understanding of C++’s type deduction rules remains essential.
CHAPTER 2 auto
Item 5: Prefer auto
to explicit type declarations.
使用 auto
的好处
- avoidance of uninitialized variables(不初始化的话, 编译器会报错).
1 | int x1; // potentially uninitialized |
- verbose variable declarations(简洁, 节省打字时间)
1 | template<typename It> // algorithm to dwim ("do what I mean") |
- the ability to directly hold closures(比
std::function
很多时候有优势)
1 | // std::function without using auto |
lambda with auto
的优势(std::function
的劣势):
- 内存使用较大:
std::function
has a fixed size for any given signature. This size may not be adequate for the closure it’s asked to store. - 较慢: the
std::function
constructor will allocate heap memory to store the closure(out-of-memory exceptions risk). 并且 thanks to implementation details that restrict inlining and yield indirect function calls, invoking a closure via astd::function
object is almost certain to be slower than calling it via anauto
-declared object. - Use lambdas instead of
std::bind
in Item 34.
- “type shortcuts”
例子1:
1 | std::vector<int> v; |
The official return type of v.size()
is std::vector<int>::size_type
, on 64-bit Windows, unsigned is 32 bits, while std::vector<int>::size_type
is 64 bits. This means that code that works under 32-bit Windows may behave incorrectly under 64-bit Windows.
例子2:
1 | std::unordered_map<std::string, int> m; |
问题: the type of std::pair
in the hash table isn’t std::pair<std::string, int>
, it’s std::pair<const std::string, int>
. 编译器因此需要强制转换 As a result, compilers will strive to find a way to convert std::pair<const std::string, int>
objects to std::pair<std::string, int>
objects. They’ll succeed by creating a temporary object of the type that p
wants to bind to by copying each object in m
, then binding the reference p
to that temporary object. At the end of each loop iteration, the temporary object will be destroyed.
With auto
if you take p
’s address, you’re sure to get a pointer to an element within m
.
机制: 错误地 explicitly specifying types can lead to implicit conversions that you neither want nor expect.
- automatically update
auto
types automatically change if the type of their initializing expression changes, and that means that some refactorings are facilitated by the use ofauto
.
Things to Remember
auto
variables must be initialized, are generally immune to type mismatches that can lead to portability or efficiency problems, can ease the process of refactoring, and typically require less typing than variables with explicitly specified types.auto
-typed variables are subject to the pitfalls described in Items 2 and 6.
Item 6: Use the explicitly typed initializer idiom when auto
deduces undesired types.
As a general rule, “invisible” proxy classes don’t play well with auto
. Objects of such classes are often not designed to live longer than a single statement, so creating variables of those types tends to violate fundamental library design assumptions.
什么是 “invisible” proxy
a proxy class: a class that exists for the purpose of emulating and augmenting the behavior of some other type.
一些在标准库/现实中的例子:
std::vector<bool>::reference
exists to offer the illusion thatoperator[]
forstd::vector<bool>
returns a reference to a bit.smart pointer types.
expression templates 技术
1
Matrix sum = m1 + m2 + m3 + m4;
This expression can be computed much more efficiently if
operator+
forMatrix
objects returns a proxy for the result instead of the result itself. 即 a proxy class such asSum<Matrix, Matrix>
instead of aMatrix
object. encode the entire initialization expression be something likeSum<Sum<Sum<Matrix, Matrix>,Matrix>, Matrix>
.
不使用 auto
时没问题, 关键是 operator[]
returns a std::vector<bool>::reference
object, which is then implicitly converted to the bool
that is needed to initialize highPriority
.
1 | std::vector<bool> features(const Widget& w); |
使用 auto
:
1 | auto highPriority = features(w)[5]; // is w high priority? |
出错原因: operator[]
for std::vector<bool>
doesn’t return T&
(which is what std::vector::operator[]
returns for every type except bool
). Instead, it returns an object of type std::vector<bool>::reference
(a class nested inside std::vector<bool>
).
为啥会这样设计 std::vector<bool>
?
C++ forbids references to bits. operator[]
for std::vector<bool>
returns an object that acts like a bool&
. 这就是 proxy design pattern 的运用, 但是是隐式的: std::vector<bool>::reference
that make this work is an implicit conversion to bool
. 因此叫做 “invisible” proxy .
为什么会产生 undefined behavior?
简而言之, features
函数会产生 temporary std::vector<bool>
object. operator[]
作用在 temporary 上会产生 std::vector<bool>::reference
object. 因此 highPriority
也是一个 std::vector<bool>::reference
类型的对象, 是个引用, 去引用 temporary, 然而 temporary 在 =
右边返回后就被销毁了, 因此导致 highPriority
指向一个悬空的对象. 原文的解释如下:
The call to features returns a temporary std::vector<bool>
object. This object has no name, but for purposes of this discussion, I’ll call it temp
. operator[]
is invoked on temp
, and the std::vector<bool>::reference
it returns contains a pointer to a word in the data structure holding the bits that are managed by temp
, plus the offset into that word corresponding to bit 5. highPriority
is a copy of this std::vector<bool>::reference
object, so highPriority
, too, contains a pointer to a word in temp
, plus the offset corresponding to bit 5. At the end of the statement, temp
is destroyed, because it’s a temporary object. Therefore, highPriority
contains a dangling pointer, and that’s the cause of the undefined behavior in the call to processWidget
.
结论
avoid code of this form:
auto someVar = expression of "invisible" proxy class type;
如何辨别 “invisible” proxy?
- documentation
- header files: Paying careful attention to the interfaces you’re using can often reveal the existence of proxy classes. 例子如下:
1 | namespace std { // from C++ Standards |
怎么解决这个问题
The solution is to force a different type deduction. explicitly typed initializer idiom –> declaring a variable with auto
, but casting the initialization expression to the type you want auto
to deduce.
1 | auto highPriority = static_cast<bool>(features(w)[5]); |
explicitly typed initializer idiom 别的用处–方便别人理解意图
例子 1: 显示地说明我进行了强制转换:
1 | double calcEpsilon(); // return tolerance value |
例子 2: 使用小数作为容器下标的例子:
1 | int index = d * c.size(); |
Things to Remember
- “Invisible” proxy types can cause auto to deduce the “wrong” type for an initializing expression.
- The explicitly typed initializer idiom forces auto to deduce the type you want it to have.
CHAPTER 3 Moving to Modern C++
Item 7: Distinguish between ()
and {}
when creating objects.
initialization
各种各样的初始化方式:
1 | int x(0); // initializer is in parentheses |
注意: 误解 equals 不一定是 assignment 如下:
1 | Widget w1; // call default constructor |
uniform initialization 的好处
C++11 introduces uniform initialization: a single initialization syntax that can, at least in concept, be used anywhere and express everything. 也被叫做Braced initialization.
- formerly inexpressible sovled
1 | std::vector<int> v{ 1, 3, 5 }; // v's initial content is 1, 3, 5 |
- specify default initialization values for non-static data members.
1 | class Widget { |
- uncopyable objects (e.g.,
std::atomics
—see Item 40) may be initialized using braces or parentheses, but not using=
:
1 | std::atomic<int> ai1{ 0 }; // fine |
- prohibits implicit narrowing conversions among built-in types
If the value of an expression in a braced initializer isn’t guaranteed to be expressible by the type of the object being initialized, the code won’t compile:
1 | double x, y, z; |
- immunity to C++’s most vexing parse
C++’s rule that anything that can be parsed as a declaration must be interpreted as one declaration.
1 | Widget w1(10); // call Widget ctor with argument 10 |
drawback to braced initialization
- Item 2 中提到的
auto
与之不兼容的问题. - 破坏构造函数中的 overload resolution, 强制带
std::initializer_list
的重载为最优先, 即便是通过 implicit conversion 也要换成std::initializer_list
导致很多违反直觉的结果.
当不存在 std::initializer_list
的重载构造函数时:
1 | class Widget { |
当存在 std::initializer_list
的重载构造函数时:
1 | class Widget { |
copy and move construction can be hijacked by std::initializer_list
constructors:
1 | class Widget { |
Compilers’ determination to match braced initializers with constructors taking std::initializer_lists
is so strong, it prevails even if the best-match std::initializer_list
constructor can’t be called. For example:
1 | class Widget { |
优先级之高导致: Only if there’s no way to convert the types of the arguments in a braced initializer to the type in a std::initializer_list
do compilers fall back on normal overload resolution.
1 | class Widget { |
edge case: “no arguments” std::initializer_list
constructor VS default constructor
Empty braces mean no arguments, not an empty std::initializer_list
:
1 | class Widget { |
实际 std::initializer_list
constructor 应用的例子:std::vector
1 | std::vector<int> v1(10, 20); // use non-std::initializer_list ctor: create 10-element |
对于开发者的建议
- as a class author, you need to be aware that if your set of overloaded constructors includes one or more functions taking a
std::initializer_list
, client code using braced initialization may see only thestd::initializer_list
overloads. As a result, it’s best to design your constructors so that the overload called isn’t affected.
再次强调: The difference with std::initializer_list constructor
overloads is that a std::initializer_list
overload doesn’t just compete with other overloads, it overshadows them to the point where the other overloads may hardly be considered. So add such overloads only with great deliberation.
In other words, learn from what is now viewed as an error in the design of the
std::vector
interface, and design your classes to avoid it.
As a class client, you must choose carefully between parentheses and braces when creating objects. 需要个人偏向的哲学. Most developers end up choosing one kind of delimiter as a default, using the other only when they have to.
()
还是{}
为主都可以.As a template author, in general, it’s not possible to know which should be used.
suppose you’d like to create an object of an arbitrary type from an arbitrary number of arguments.
1 | template<typename T, // type of object to create |
There are two ways to turn the line of pseudocode into real code:
1 | T localObject(std::forward<Ts>(params)...); // using parens |
If doSomeWork
uses parentheses when creating localObject
, the result is a std::vector
with 10 elements. If doSomeWork
uses braces, the result is a std::vector
with 2 elements. Which is correct? The author of doSomeWork
can’t know. Only the caller can.
现实中这个问题被遇到过: This is precisely the problem faced by the Standard Library functions std::make_unique
and std::make_shared
. 解决方法: internally using parentheses and by documenting this decision as part of their interfaces.
Things to Remember
- Braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it’s immune to C++’s most vexing parse.
- During constructor overload resolution, braced initializers are matched to
std::initializer_list
parameters if at all possible, even if other constructors offer seemingly better matches. - An example of where the choice between parentheses and braces can make a significant difference is creating a
std::vector<numeric type>
with two arguments. - Choosing between parentheses and braces for object creation inside templates can be challenging.
Item 8: Prefer nullptr
to 0 and NULL
.
0 与 NULL
的问题: 既是整数又可以被转化为指针
C++’s primary policy is that 0 is an int
, not a pointer.
Implementations are allowed to give NULL
an integral type other than int
(e.g., long
), also not a pointer.
Passing 0 or NULL
to such overloads never called a pointer overload:
1 | void f(int); // three overloads of f |
f(NULL);
有可能编不过的原因:
If NULL
is defined to be, say, 0L
(i.e., 0 as a long
), the call is ambiguous, because conversion from long
to int
, long
to bool
, and 0L
to void*
are considered equally good.
guideline for C++98 programmers to avoid overloading on pointer and integral types.
nullptr
的好处
nullptr
’s advantage is that it doesn’t have an integral type.nullpt
r’s actual type is std::nullptr_t
.
The type std::nullptr_t
implicitly converts to all raw pointer types(也就是说 implicitly converts to all pointer types).
Calling the overloaded function f
with nullptr
calls the void*
overload:
1 | f(nullptr); // calls f(void*) overload |
用处举例 1: 消除歧义
1 | auto result = findRecord( /* arguments */ ); |
用处2: template type deduction deduces the “wrong” types for 0 and NULL
1 | //函数声明 |
0 与 NULL
出现 error 的原因是编译器会优先把它们推断成 int
(long
) 型与函数 f1
f2
声明的智能指针不匹配.
Things to Remember
- Prefer
nullptr
to 0 andNULL
. - Avoid overloading on integral and pointer types.
Item 9: Prefer alias declarations to typedef
s.
一般场景下区别不明显:
1 | typedef |
区别在模板使用上:
In particular, alias declarations may be templatized (in which case they’re called alias templates), while typedef
s cannot.
typedef
s nested inside templatized structs. 需要手动把模板推导功能再来一遍.
1 | template<typename T> |
- dependent type: must be preceded by
typename
. 使用 alias 会使得MyAllocList<T>
is a non-dependent type, 确定一定是类型而不是成员变量, 因此不需要typename
.
1 | template<typename T> |
来自 Standardization Committee 的背书
C++11 gives you the tools to perform type transformations in the form of type traits(<type_traits>
).
1 | std::remove_const<T>::type // yields T from const T |
C++11 由于历史原因没有使用 alias template. type traits are implemented as nested typedef
s inside templatized structs, 因此 ::type
at the end of each use. you’d also have to precede each use with typename
.
但是到了 C++14 就用 alias template 修复了这个问题(std::transformation_t
), 标准都这么做了, 程序员也应该这么做.
1 | std::remove_const<T>::type // C++11: const T → T |
在 C++ 14 中实现 std::transformation_t
所用的 alias template.
1 | template <class T> |
Things to Remember
typedefs
don’t support templatization, but alias declarations do.- Alias templates avoid the
::type
suffix and, in templates, thetypename
prefix often required to refer totypedef
s. - C++14 offers alias templates for all the C++11 type traits transformations.
Item 10: Prefer scoped enum
s to unscoped enum
s.
As a general rule, declaring a name inside curly braces {}
limits the visibility of that name to the scope defined by the braces.
C++98 中的例外: enum
s. The names of such enumerators belong to the scope containing the enum
, and that means that nothing else in that scope may have the same name:
1 | enum Color { black, white, red }; // black, white, red are in same scope as Color |
这种例外有专用的名称: enum
definition gives rise to the official term for this kind of enum
: unscoped.
C++11 中有 scoped enum
s, they’re sometimes referred to as enum
classes.
1 | enum class Color { black, white, red }; // black, white, red are scoped to Color |
应用: implicit conversions to int
1 | enum Color { black, white, red }; // unscoped enum |
no implicit conversions from enumerators in a scoped enum
to any other type:
1 | enum class Color { black, white, red }; // enum is now scoped |
如果真的想转换:
1 | if (static_cast<double>(c) < 14.5) { // odd code, but it's valid |
scoped enum
s may be forward-declared:
1 | enum Color; // error! |
背后的原理是怎样的呢?
compiler 的动机: To make efficient use of memory, compilers often want to choose the smallest underlying type for an enum
that’s sufficient to represent its range of enumerator values.
编译器为了知道 enum
需要的内存大小, 必须在使用它时将其定义好. 但是副作用是 increase in compilation dependencies.
1 | enum Status { good = 0, |
If a new status value is then introduced,
1 | enum Status { good = 0, |
it’s likely that the entire system will have to be recompiled, even if only a single subsystem—possibly only a single function!—uses the new enumerator.
与之相对的是 forward declaration 下的 scoped enum
s 不会, 只会在使用新加 value 地方需要重新编译.
1 | enum class Status; // forward declaration |
那 scoped enum
s 是怎么做到这一点的呢?
The answer is simple: the underlying type for a scoped enum
is always known, and for unscoped enum
s, you can specify it.
By default, the underlying type for scoped enum
s is int
. If the default doesn’t suit you, you can override it.
1 | enum class Status; // underlying type is int DEFAULT |
To specify the underlying type for an unscoped enum
, you do the same thing as for a scoped enum
, and the result may be forward-declared:
1 | enum Color: std::uint8_t; // fwd decl for unscoped enum; underlying type is std::uint8_t |
对一种 unscoped enum
s 的用武之地的 scope enum
s 化
there’s at least one situation where unscoped enum
s may be useful. That’s when referring to fields within C++11’s std::tuples
.
1 | using UserInfo = |
谁能记得住 UserInfo
的第一字段的具体内容, 这种场景还是 unscoped enum
s 比较好用:
1 | enum UserInfoFields { uiName, uiEmail, uiReputation }; |
使用 scoped enum
s 没有那么简洁(substantially more verbose):
1 | enum class UserInfoFields { uiName, uiEmail, uiReputation }; |
那要如何改进 scope enum
不用这么复杂呢?
这依赖于下面三个想法:
- return the
enum
’s underlying type via thestd::underlying_type
type trait. - use
constexpr
to produce its result during compilation. - it will never yield an exception –> declare it
noexcept
.
C++11 下对 scope enum
的改进
1 | template<typename E> |
C++14 下使用 std::underlying_type_t
1 | template<typename E> // C++14 |
C++14 进一步使用 auto
:
1 | template<typename E> // C++14 |
使用改造后 scoped enum
的例子:
1 | auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo); |
Things to Remember
- C++98-style
enum
s are now known as unscopedenums
. - Enumerators of scoped
enum
s are visible only within theenum
. They convert to other types only with a cast. - Both scoped and unscoped
enum
s support specification of the underlying type. The default underlying type for scopedenum
s isint
. Unscopedenum
s have no default underlying type. - Scoped
enum
s may always be forward-declared. Unscopedenum
s may be forward-declared only if their declaration specifies an underlying type.
Item 10: Prefer scoped enum
s to unscoped enum
s.
namespace pollution
C++ 98 下的 enum
与常规的 {}
scope 规则相违背, 导致 namespace pollution ==> unscoped enum
s.
C++ 11 新加了 scoped enum
s.
具体描述: As a general rule, declaring a name inside curly braces limits the visibility of that name to the scope defined by the braces. Not so for the enumerators declared in C++98-style enum
s. The names of such enumerators belong to the scope containing the enum
, and that means that nothing else in that scope may have the same name:
1 | enum Color { black, white, red }; |
nonsensical implicit type conversions
由于 unscoped enum
s implicitly convert to integral types (and, from there, to floating-point types). 导致不可控的类型转换场景出现. 为了精确掌控类型转换 no implicit conversions from enumerators in a scoped enum
to any other type. 必须经过显式的类型转换.
1 | enum Color// unscoped enum |
forward-declaration
unscoped enum
: compilers choose different underlying types for different cases( make efficient use of memory or optimize for speed instead of size) ==> 无法知道内存布局 ==> 很多时候无法进行 forward-declarate.
当然也可以指定其 underlying type 以实现 forward-declaration.
scoped enum
: the underlying type is always known.
无法进行 forward-declarate 的问题: increase in compilation dependencies.
1 | enum Status |
This is the kind of enum that’s likely to be used throughout a system, hence included in a header file that every part of the system is dependent on. If a new status value is then introduced, it’s likely that the entire system will have to be recompiled, even if only a single subsystem—possibly only a single function!—uses the new enumerator.
有了 forward-declaration 则不需要 recompilation 过多.
1 | enum class Status; // forward declaration |
指定 underlying type 以实现 forward-declaration.
1 | enum Color : std::uint8_t;// fwd decl for unscoped enum underlying type is std::uint8_t |
drawback
有些时候 unscoped enum
的 implicit conversion 还是比较方便的, 换成 scoped enum
可能没那么方便. trade-off: 利便性 VS 正确性.
例如 std::tuple
的 get<>
模板函数的访问:
1 | using UserInfo = |
切换为 scoped enum
1 | // scoped enums is substantially more verbose: |
make that generalization: generalize the return type, too. Rather than returning std::size_t
, we’ll return the enum
’s underlying type.
构造 a function template toUType
that takes an arbitrary enumerator and can return its value as a compile-time constant:
1 | template <typename E> // c++11 |
Things to Remember
- C++98-style
enum
s are now known as unscopedenum
s. - Enumerators of scoped
enum
s are visible only within theenum
. They convert to other types only with a cast. - Both scoped and unscoped
enum
s support specification of the underlying type. The default underlying type for scopedenum
s isint
. Unscopedenum
s have no default underlying type. - Scoped
enum
s may always be forward-declared. Unscopedenum
s may be forward-declared only if their declaration specifies an underlying type.
Item 11: Prefer delete
d functions to private undefined ones.
C++98 的做法: 声明为 private 并且不提供定义:
1 | template <class charT, class traits = char_traits<charT> > |
实现的效果有2点:
- Declaring these functions private prevents clients from calling them.
- member functions or friends of the classuses them, linking will fail due to missing function definitions.
C++11 使用 delete
, 将其改造为 deleted functions.
1 | template <class charT, class traits = char_traits<charT> > |
与 C++98 做法相比有如下改进:
- member and friend functions will fail to compile.
- deleted functions are
declared
public
, notprivate
. 原因是private
的优先级更高(some compilers complain only about the function being private). 这导致了更本质的调用错误信息delete
无法被显示出来.(making the new functions public will generally result in better error messages)
相比 private member function delete
的好处是:
1. any function may be deleted, while only member functions may be private.
1 | bool isLucky(int number); |
这种做法的细节:
- overload is deleted will prevent the call from compiling.
- 利用 implicit conversion 的规则可以少声明
delete
语句. 例如float
会优先默认转化成double
而不是int
, 所以不用声明bool isLucky(float) = delete;
2. prevent use of template instantiations that should be disabled
限制模板实例化的参数类型:
it should not be possible to call processPointer
with void*
or char*
pointers.
1 | template<typename T> |
There are two special cases in the world of pointers.
- One is
void*
pointers, because there is no way to dereference them, to increment or decrement them, etc. - The other is
char*
pointers, because they typically represent pointers to C-style strings, not pointers to individual characters.
更多需要禁的细节: delete the const volatile void*
and const volatile char*
overloads. overloads for pointers to the other standard character types: std::wchar_t
, std::char16_t
, and std::char32_t
.
3. template function in class
声明 class 里的 template specializations 为 private 不可行的原理: it’s not possible to give a member function template specialization a different access level from that of the main template.
1 | class Widget { |
The problem is that template specializations must be written at namespace scope, not class scope. This issue doesn’t arise for deleted functions, because they don’t need a different access level. They can be deleted outside the class (hence at namespace scope):
1 | class Widget { |
Things to Remember
- Prefer deleted functions to private undefined ones.
- Any function may be deleted, including non-member functions and template instantiations.
Item 12: Declare overriding functions override.
For overriding to occur, several requirements must be met:
- The base class function must be virtual.
- The base and derived function names must be identical (except in the case of destructors).
- The parameter types of the base and derived functions must be identical.
- The constness of the base and derived functions must be identical.
- The return types and exception specifications of the base and derived functions must be compatible.
对于 C++11 增加了 requirements
- The functions’ reference qualifiers must be identical.
1 | class Widget { |
编译器提示
You can’t rely on compilers notifying you if you do something wrong. C++ 11 declare it override
: enable compilers to tell you when would-be overrides aren’t overriding anything.
1 | class Base { |
方便测试
利用 compiler 报错可以 help you gauge the ramifications(分枝) if you’re contemplating changing the signature of a virtual function in a base class.
具体地: If derived classes use override
everywhere, you can just change the signature, recompile your system, see how much damage you’ve caused (i.e., how many derived classes fail to compile), then decide whether the signature change is worth the trouble.
contextual keywords
C++11 introduces two contextual keywords, override
and final
. 保障一定程度上的向后兼容性.
These keywords have the characteristic that they are reserved, but only in certain contexts. In the case of override
, it has a reserved meaning only when it occurs at the end of a member function declaration. That means that if you have legacy code that already uses the name override
, you don’t need to change it for C++11:
1 | class Warning { // potential legacy class from C++98 |
more about member function reference qualifiers
例如提供数据给 client 修改, 先是左值引用的版本
1 | class Widget { |
如果是右值引用的情况下:
1 | Widget makeWidget(); |
the Widget
is the temporary object returned from makeWidget
(i.e., an rvalue), so copying the std::vector
inside it is a waste of time.
虽然编译器优化可以识别这种 copy 然后不采用 copy 而是 move 之. 作者的观点是不能依赖于优化.
The rules of C++ require that compilers generate code for a copy. (There’s some wiggle room for optimization through what is known as the “as if rule,” but you’d be foolish to rely on your compilers finding a way to take advantage of it.)
使用左右值引用重载:
1 | class Widget { |
Things to Remember
- Declare overriding functions override.
- Member function reference qualifiers make it possible to treat lvalue and rvalue objects (
*this
) differently.
Item 13: Prefer const_iterator
s to iterator
s.
const_iterator
s are the STL equivalent of pointers-to-const
.
C++98 里为啥 const_iterator
s 不受欢迎?
- 获取困难: there was no simple way to get a
const_iterator
from anon-const
container.
虽然也有别的方法实现这种转换, (e.g., you could bind values to areference-to-const
variable, then use that variable in place of values in your code).
下面是通过 casting 的形式实现:
1 | // 不使用 const_iterator |
const_iterator
s 到iterator
的转换: non-portable
There’s no portable conversion from a const_iterator
to an iterator, not even with a static_cast
.
Even the semantic sledgehammer known as reinterpret_cast
can’t do the job.
即便是在 C++11 也无法实现 portable conversion: It’s true in C++11, too. const_iterator
s simply don’t convert to iterators, no matter how much it might seem like they should.
C++11 的进步
- 获取变简单了: The container member functions
cbegin
andcend
produceconst_iterators
. - 使用场景被拓宽了: even for non-const containers, and STL member functions that use iterators to identify positions (e.g.,
insert
anderase
) actually useconst_iterators
.
1 | std::vector<int> values; // as before |
C++11 的待完善
Should takes into account that some containers and container-like data structures offer begin
and end
(plus cbegin
, cend
, rbegin
, etc.) as non-member functions, rather than members. 但是不是所有的库都提供 members, 例如 built-in arrays.
解决办法: 自定义一个 non-member template function(实现 Maximally generic code) 并且去调用已经存在的其他 non-member function:
1 | template <class C> |
注意: 为什么 return std::begin()
而不是 return std::cbegin()
?
- 很多
container
有std::begin()
没有std::cbegin()
. - 能够通过推导实现: Invoking the non-member
begin
function (provided by C++11) on a const container yields aconst_iterator
. - also works if
C
is a built-in array type. 可以参照 Item 1 type deduction in templates that take reference parameters to arrays.
C++14 的完善
C++14 补足了 std::cbegin
, std::cend
, std::rbegin
, std::rend
, std::crbegin
, and std::crend
.
1 | // in container, find first occurrence of targetVal, then insert insertVal there |
Things to Remember
- Prefer
const_iterators
to iterators. - In maximally generic code, prefer non-member versions of
begin
,end
,rbegin
, etc., over their member function counterparts.
Item 14: Declare functions noexcept
if they won’t emit exceptions.
C++ 11 对异常指定的改进
maybe-or-never dichotomy forms(Black or white 非黑即白) the basis of C++11’s exception specifications.
C++98-style exception specifications remain valid, but they’re deprecated.
改进的好处 1: 更容易判断一个函数的异常抛出状态
Callers can query a function’s noexcept status, and the results of such a query can affect the exception safety or efficiency of the calling code.
whether a function is noexcept
is as important a piece of information as whether a member function is const
.
改进的好处 2: permits compilers to generate better object code
例如对于指定不抛出异常的函数遇到异常后的处理过程: 区别在于 unwinding the call stack and possibly unwinding it
1 | int f(int x) throw();// no exceptions from f: C++98 style |
- the C++98 exception specification: the call stack is unwound to
f
’s caller, and, after some actions not relevant here, program execution is terminated. - the C++11 exception specification: runtime behavior is slightly different: the stack is only possibly unwound before program execution is terminated.
优化的机制: In a noexcept
function, optimizers need not keep the runtime stack in an unwindable state if an exception would propagate out of the function, nor must they ensure that objects in a noexcept
function are destroyed in the inverse order of construction should an exception leave the function.
优化等级如下:
1 | RetType function(params) noexcept; // most optimizable |
改进的好处 3: 帮助明确可以使用 move semantics
很多 STL 算法提供了对 move semantics 的支持, 但是考虑到异常相关的安全问题, 采用的策略是 “move if you can, but copy if you must” 例如 std::vector::push_back
.
所谓的异常相关的安全问题拆解如下:
1 | std::vector<Widget> vw; |
当 vw
的容量上限达到了, 再去 push_back
会导致扩容以及”移动”旧的元素. 如果是 copy 的话, 即便是在新的内存位置上构建元素出现了异常也不会影响到旧的内存以及上面的元素. 但是 move 的话就不行, 异常的产生会导致 UB. 因此编译器希望判断 move 行为是不会抛出异常的才会进行 move, 而 noecept
是最好的信号.
原文如下:
the std::vector
allocates a new, larger, chunk of memory to hold its elements, and it transfers the elements from the existing chunk of memory to the new one.
In C++98, the transfer was accomplished by copying each element from the old memory to the new memory, then destroying the objects in the old memory. This approach enabled push_back to offer the strong exception safety guarantee: if an exception was thrown during the copying of the elements, the state of the std::vector
remained unchanged, because none of the elements in the old memory were destroyed until all elements had been successfully copied into the new memory.
In C++11 move: If $n$ elements have been moved from the old memory and an exception is thrown moving element $n+1$, the push_back
operation can’t run to completion. But the original std::vector
has been modified: $n$ of its elements have been moved from. Restoring their original state may not be possible, because attempting to move each object back into the original memory may itself yield an exception.
C++98 的 push_back
是 copy 的做法, 因此对异常是很安全的, C++11 希望增加移动语义到 push_back
中, 就需要判断 move 的过程是否会产生异常, 如果不会就 move, 如果可能会产生异常就采用 C++98 的 copy 的做法.
the behavior of legacy code could depend on push_back
’s strong exception safety guarantee. Therefore, C++11 implementations can’t silently replace copy operations inside push_back
with moves unless it’s known that the move operations won’t emit exceptions.
conditionally noexcept
1 | template <class T, size_t N> |
可以看到出现了 noexcept
的嵌套, 原因: swapping higher-level data structures can generally be noexcept
only if swapping their lower-level constituents is noexcept
should motivate you to offer noexcept
swap
functions whenever you can.
对于是否应用 noexcept
的讨论
- 添加/去除
noexcept
break interface, 可能导致 client codes 出问题.
原则上接口是长期政策而不能随意修改的: noexcept is part of a function’s interface, so you should declare a function noexcept only if you are willing to commit to a noexcept implementation over the long term.
- exception-neutral 的概念
大多数函数都是中立的, 只负责 propagate exception. 因此不能随意声明 noexcept
.
The fact of the matter is that most functions are exception-neutral. Such functions throw no exceptions themselves, but functions they call might emit one. When that happens, the exception-neutral function allows the emitted exception to pass through on its way to a handler further up the call chain. Exception-neutral functions are never noexcept
.
- 像
swap
等少量函数必须坚持noexcept
a few more—notably the move operations and swap —being noexcept
can have such a significant payoff.
STL 里面的大多数函数都是 noexcept
的了, 从 C++11 到 C++20 陆陆续续地都被修改了.
对于析构以及内存释放:
In C++98, it was considered bad style to permit the memory deallocation functions (i.e., operator delete
and operator delete[]
) and destructors to emit exceptions,
and in C++11, this style rule has been all but upgraded to a language rule. By default, all memory deallocation functions and all destructors—both user-defined and compiler-generated—are implicitly noexcept
.
noexcept(false)
如果硬要声明一个析构函数可以抛出异常.
- distinguish functions with wide contracts from those with narrow contracts
定义: A function with a wide contract has no preconditions. Such a function may be called regardless of the state of the program, and it imposes no constraints on the arguments that callers pass it. Functions with wide contracts never exhibit undefined behavior. ==> noexcept
Functions without wide contracts have narrow contracts. For such functions, if a precondition is violated, results are undefined. ==> Debugging an exception(“precondition was violated”) that’s been thrown is generally easier than trying to track down the cause of undefined behavior. ==> maybe not noexcept
.
- 编译器对异常使用与否的判断无能为力
1 | void setup(); // functions defined elsewhere |
Because there are legitimate reasons for noexcept
functions to rely on code lacking the noexcept
guarantee, C++ permits such code, and compilers generally don’t issue warnings about it.
Things to Remember
noexcept
is part of a function’s interface, and that means that callers may depend on it.noexcept
functions are more optimizable than non-noexcept functions.noexcept
is particularly valuable for the move operations,swap
, memory deallocation functions, and destructors.- Most functions are exception-neutral rather than
noexcept
.
Item 15: Use constexpr
whenever possible.
Conceptually, constexpr
indicates a value that’s not only constant, it’s known during compilation.
但是, 上面只是理想状态下, you can’t assume that the results of constexpr
functions are const
, nor can you take for granted that their values are known during compilation. Perhaps most intriguingly, these things are features. It’s good that constexpr
functions need not produce results that are const
or known during compilation!
为了搞明白上面的说法, 开启本节内容.
纠正一个理论上的误区: Technically, constexpr
values are determined during translation, and translation consists not just of compilation but also of linking.
integral constant expression
It includes specification of array sizes, integral template arguments (including lengths of std::array
objects), enumerator values, alignment specifiers, and more. => declare it constexpr
because compilers will ensure that it has a compile-time value.
1 | int sz; // non-constexpr variable |
const
与 constexpr
const
无法胜任 constexpr
: const
objects need not be initialized with values known during compilation.
1 | int sz; // as before |
All
constexpr
objects areconst
, but not allconst
objects areconstexpr
.
constexpr
functions 的双面性
- 编译期:
constexpr
functions can be used in contexts that demand compile-time constants. If the values of the arguments you pass to aconstexpr
function in such a context are known during compilation, the result will be computed during compilation. - 运行期: When a
constexpr
function is called with one or more values that are not known during compilation, it acts like a normal function, computing its result at runtime.
constexpr
的限制
return
C++11 中 constexpr
functions may contain no more than a single executable statement: a return
.
解决第三个办法利用 2 种技术:
- 三目运算符
- 递归
1 | constexpr int pow(int base, int exp) noexcept |
C++14 取消了这个限制:
1 | constexpr int pow(int base, int exp) noexcept // C++14 |
literal types
constexpr
functions are limited to taking and returning literal types(types that can have values determined during compilation).
literal types 包括:
- In C++11, all built-in types except
void
qualify, - but user-defined types may be literal, too, because constructors and other member functions may be
constexpr
:
1 | class Point { |
constexpr
的威力, you could use an expression like mid.xValue() * 10
in an argument to a template or in an expression specifying the value of an enumerator!
C++14 的进一步改进
上面的 setX
and setY
函数无法设定为 constexpr
反映了 C++11 在这上面的缺陷:
- they modify the object they operate on, and in C++11,
constexpr
member functions are implicitlyconst
. - they have
void
return types, andvoid
isn’t a literal type in C++11.
C++14 解除了这些限制:
1 | class Point { |
Things to Remember
constexpr
objects areconst
and are initialized with values known during compilation.constexpr
functions can produce compile-time results when called with arguments whose values are known during compilation.constexpr
objects and functions may be used in a wider range of contexts thannon-constexpr
objects and functions.constexpr
is part of an object’s or function’s interface.
Item 16: Make const
member functions thread safe.
并不是说 const
member function 一般只涉及到读不改就不用考虑 thread safe 设计(因为有 mutable
成员变量的存在).
从一个例子介绍, 计算多项式的根, 因为计算代价比较高, 我们希望计算以后 cache 后面直接取用即可, 实现代码如下:
1 | class Polynomial { |
roots()
虽然被声明为 const
但是由于它需要去第一次计算的时候更改 rootVals
, 因此把两个 member data 都设置成了 mutable
.
多线程使用 root()
可能出现 data race.
1 | Polynomial p; |
The problem is that roots
is declared const
, but it’s not thread safe.
解决办法有 2:
std::mutex
std::atomic
先是第一个:
1 | class Polynomial { |
第二个的例子, 计算对象函数的计算次数:
1 | class Point { // 2D point |
std::mutex
与 std::atomic
的区别
- 一般而言,
std::atomic
is less expensive. (Whether it actually is less expensive depends on the hardware you’re running on and the implementation of mutexes in your Standard Library.) std::atomic
不适合多个数据体的情况.
1 | class Widget { |
This will work, but sometimes it will work a lot harder than it should.
- A thread calls
Widget::magicValue
, seescacheValid
as false, performs the two expensive computations, and assigns their sum tocachedValue
. - At that point, a second thread calls
Widget::magicValue
, also seescacheValid
as false, and thus carries out the same expensive computations that the first thread has just finished. (This “second thread” may in fact be several other threads.)
自然而然地想到如下改进方法(调换执行顺序):
1 | class Widget { |
即调换 part 1 与 part 2 的顺序. 但是这么做会导致错误:
- One thread calls
Widget::magicValue
and executes through the point wherecacheValid
is set to true. - At that moment, a second thread calls
Widget::magicValue
and checkscacheValid
. Seeing it true, the thread returnscachedValue
, even though the first thread has not yet made an assignment to it. The returned value is therefore incorrect.
应该使用 std::mutex
1 | class Widget { |
side effect of std::mutex
std::atom
They are move-only type (i.e., a type that can be moved, but not copied), a side effect is that object loses the ability to be copied.
Things to Remember
- Make
const
member functions thread safe unless you’re certain they’ll never be used in a concurrent context. - Use of
std::atomic
variables may offer better performance than a mutex, but they’re suited for manipulation of only a single variable or memory location.
Item 17: Understand special member function generation.
本节主要讲解 move operation(move constructor, move assignment), copy operation(copy constructor, copy assignment), 以及 destructor的
具体机理: memberwise , move implicitly converts to copy and so forth
相互依存关系
C++98 与 C++11 对比上面两条的变化
基础介绍
In official C++ parlance, the special member functions are the ones that C++ is willing to generate on its own.
C++98 has four such functions:
- the default constructor
- the destructor
- the copy constructor
- the copy assignment operator
Generated special member functions are implicitly public and inline, and they’re nonvirtual
例外: unless the function in question is a destructor in a derived class inheriting from a base class with a virtual destructor. In that case, the compiler-generated destructor for the derived class is also virtual.
C++11 新增2个:
- the move constructor
- the move assignment operator
1 | class Widget { |
move operation 机理
Move operations perform “memberwise moves” on the non-static data members of the class.
The move constructor also move-constructs its base class parts (if there are any).
there is no guarantee that a move will actually take place “Memberwise moves” are, in reality, more like memberwise move requests, because types that aren’t move-enabled (i.e., that offer no special support for move operations, e.g., most C++98 legacy classes) will be “moved” via their copy operations.
更深层一些 The heart of each memberwise “move” is application of
std::move
to the object to be moved from, and the result is used during function overload resolution to determine whether a move or a copy should be performed.
依赖关系 in C++ 11
The two copy operations are independent: declaring one doesn’t prevent compilers from generating the other.
The two move operations are not independent. If you declare either, that prevents compilers from generating the other.
The rationale is that if you declare, say, a move constructor for your class, you’re indicating that there’s something about how move construction should be implemented that’s different from the default memberwise move that compilers would generate. And if there’s something wrong with memberwise move construction, there’d probably be something wrong with memberwise move assignment, too.move operations won’t be generated for any class that explicitly declares a copy operation.
compilers figure that if memberwise copy isn’t appropriate for the copy operations, memberwise move probably isn’t appropriate for the move operations.Declaring a move operation (construction or assignment) in a class causes compilers to disable the copy operations.
Rule of Three
Rule of Three: if you declare any of a copy constructor, copy assignment operator, or destructor, you should declare all three.
- in C++98, the existence of a user-declared destructor had no impact on compilers’ willingness to generate copy operations. That continues to be the case in C++11, but only because restricting the conditions under which the copy operations are generated would break too much legacy code.
- C++11 does not generate move operations for a class with a user-declared destructor.
小结一下 So move operations are generated for classes (when needed) only if these three things are true:
- No copy operations are declared in the class.
- No move operations are declared in the class.
- No destructor is declared in the class.
我制作了一个简图描绘这些依赖关系(依赖指的是, 前者手动指定的时候后者不能自动生成):
=default
的使用
因为语法原因无法使用自动生成的时候, 显式地声明纠正语法不符合需求.
because C++11 deprecates the automatic generation of copy operations for classes declaring copy operations or a destructor.
1 | class Widget { |
什么情况下比较常用?
- polymorphic base classes => virtual destructors
Often, the default implementation would be correct, and = default
is a good way to express that.
1 | class Base { |
- side effect of declaring a destructor
1 | class StringTable { |
This looks reasonable, but declaring a destructor has a potentially significant side effect: it prevents the move operations from being generated.
move 被迫通过 copy 进行, 而 copy std::map<int, std::string>
objects 与 move 它不是一个计算级别的.
所有自动生成 member function 的总结
- Default constructor: Same rules as C++98. Generated only if the class contains no user-declared constructors.
- Destructor: Essentially same rules as C++98; sole difference is that destructors are noexcept by default. As in C++98, virtual only if a base class destructor is virtual.
- Copy constructor: Same runtime behavior as C++98: memberwise copy construction of non-static data members. Generated only if the class lacks a user-declared copy constructor. Deleted if the class declares a move operation. Generation of this function in a class with a user-declared copy assignment operator or destructor is deprecated.
- Copy assignment operator: Same runtime behavior as C++98: memberwise copy assignment of non-static data members. Generated only if the class lacks a user-declared copy assignment operator. Deleted if the class declares a move operation. Generation of this function in a class with a user-declared copy constructor or destructor is deprecated.
- Move constructor and move assignment operator: Each performs memberwise moving of non-static data members. Generated only if the class contains no user-declared copy operations, move operations, or destructor.
existence of a member function template
类内部的 template 函数不影响上面的规则.
1 | class Widget { |
compilers will still generate copy and move operations for Widget
(assuming the usual conditions governing their generation are fulfilled), even though these templates could be instantiated to produce the signatures for the copy constructor and copy assignment operator.
Things to Remember
- The special member functions are those compilers may generate on their own: default constructor, destructor, copy operations, and move operations.
- Move operations are generated only for classes lacking explicitly declared move operations, copy operations, and a destructor.
- The copy constructor is generated only for classes lacking an explicitly declared copy constructor, and it’s deleted if a move operation is declared.
The copy assignment operator is generated only for classes lacking an explicitly declared copy assignment operator, and it’s deleted if a move operation is declared.
Generation of the copy operations in classes with an explicitly declared destructor is deprecated. - Member function templates never suppress generation of special member functions.
《effective modern C++》Chapter 1-3