《effective modern C++》Chapter 1-3

《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 for decltype.
  • C++14 then extends the usage contexts in which auto and decltype 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
2
3
template<typename T>
void f(ParamType param);
f(expr);

compilers use expr to deduce two types:

  1. one for T and
  2. 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
2
3
4
template<typename T>
void f(const T& param); // ParamType is const T&
int x = 0;
f(x);// call f with an int

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

推导规则:

  1. If expr’s type is a reference, ignore the reference part.
  2. Then pattern-match expr’s type against ParamType to determine T.

param is a reference

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void f(T& param); // param is a reference

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // T is int, param's type is int&
f(cx); // T is const int, param's type is const int&
f(rx); // T is const int, param's type is const int&
// rx’s reference-ness is ignored during type deduction.

注意: 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 constness of the object becomes part of the type deduced for T. 也就是在引用的类型推导不改变 constness.

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
2
3
4
5
6
7
8
9
10
template<typename T>
void f(const T& param); // param is now a ref-to-const

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&

param is a pointer

1
2
3
4
5
6
7
8
template<typename T>
void f(T* param); // param is now a pointer

int x = 27;
const int *px = &x; // px is a ptr to x as a const int

f(&x); // T is int, param's type is int*
f(px); // T is const int, param's type is const int*

Case 2: ParamType is a Universal Reference

  1. If expr is an lvalue, both T and ParamType 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.
  2. If expr is an rvalue, the “normal” (i.e., Case 1) rules apply.

1
2
3
4
5
6
7
8
9
template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(rx); // rx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is rvalue, so T is int, param's type is therefore int&&

Case 3: ParamType is Neither a Pointer nor a Reference(pass-by-value)

推导规则:

  1. As before, if expr’s type is a reference, ignore the reference part.
  2. If, after ignoring expr’s reference-ness, expr is const or volatile, ignore that, too.
1
2
3
4
5
6
7
8
9
template<typename T>
void f(T param); // param is now passed by value

int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int

注意: cxrxconstness 被削减掉了.
原因: why expr’s constness (and volatileness, 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
2
3
4
template<typename T>
void f(T param); // param is still passed by value
const char* const ptr = // ptr is const pointer to const object "Fun with pointers";
f(ptr); // pass arg of type const char * const

推导的结果: 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
2
3
4
5
6
const char name[] = "J. P. Briggs"; // name's type is const char[13]
const char * ptrToName = name; // array decays to pointer

template<typename T>
void f(T param); // template with by-value parameter
f(name); // name is array, but T deduced as const char*

pass-by-reference

functions can declare parameters that are references to arrays.
原理: springing from the C roots at the base of C++.

1
2
3
4
template<typename T>
void f(T& param); // template with by-reference parameter

f(name); // pass array to f

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
2
3
4
5
6
7
8
// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template<typename T, std::size_t N> // see info below on and noexcept
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}

用处: declaring this function constexpr makes its result available during compilation.

1
2
3
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };//keyVals has 7 elements
int mappedVals[arraySize(keyVals)];//so does mappedVals
std::array<int, arraySize(keyVals)> mappedVals;//so does mappedVals

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
2
3
4
5
6
7
void someFunc(int, double); // someFunc is a function; type is void(int, double)
template<typename T>
void f1(T param); // in f1, param passed by value
template<typename T>
void f2(T& param); // in f2, param passed by ref
f1(someFunc); // param deduced as ptr-to-func; type is void (*)(int, double)
f2(someFunc); // param deduced as ref-to-func; 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
auto x = 27; // case 3 (x is neither ptr nor reference)
const auto cx = x; // case 3 (cx isn't either)
const auto& rx = x; // case 1 (rx is a non-universal ref.)

template<typename T> // conceptual template for deducing x's type
void func_for_x(T param);
func_for_x(27); // conceptual call: param's deduced type is x's type

template<typename T> // conceptual template for deducing cx's type
void func_for_cx(const T param);
func_for_cx(x); // conceptual call: param's deduced type is cx's type

template<typename T> // conceptual template for deducing rx's type
void func_for_rx(const T& param);
func_for_rx(x); // conceptual call: param's deduced type is rx's type

//Case 2
auto&& uref1 = x; // x is int and lvalue, so uref1's type is int&
auto&& uref2 = cx; // cx is const int and lvalue, so uref2's type is const int&
auto&& uref3 = 27; // 27 is int and rvalue, so uref3's type is int&&

array and function names decay into pointers for non-reference type specifiers:

1
2
3
4
5
6
7
const char name[] = "R. N. Briggs"; // name's type is const char[13] 
auto arr1 = name; // arr1's type is const char*
auto& arr2 = name; // arr2's type is const char (&)[13]

void someFunc(int, double); // someFunc is a function; type is void(int, double)
auto func1 = someFunc; // func1's type is void (*)(int, double)
auto& func2 = someFunc; // func2's type is void (&)(int, double)

与模板推断不同的地方

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
2
3
4
5
6
7
8
9
10
11
12
13
14
//C++98:
int x1 = 27;
int x2(27);
//C++11:
int x3 = { 27 };
int x4{ 27 };
//Auto:
auto x1 = 27; // type is int, value is 27
auto x2(27); // ditto
auto x3 = { 27 }; // type is std::initializer_list<int>, value is { 27 }
auto x4{ 27 }; // ditto

//需要符合 std::initializer_list<T> 的构造规则
auto x5 = { 1, 2, 3.0 }; // error! can't deduce T for std::initializer_list<T>

但是 autostd::initializer_list 的推断也不是无限层次的(自己的理解, 原文未提及), 例如:

1
2
3
4
5
6
7
8
template<typename T> // template with parameter
void f(T param); // declaration equivalent to x's declaration
f({ 11, 23, 9 }); // error! can't deduce type for T

//手动指定 std::initializer_list<T> 推导
template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); // T deduced as int, and initList's type is std::initializer_list<int>

结论: 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

新增的功能:

  1. permits auto to indicate that a function’s return type should be deduced.
1
2
3
4
auto createInitList()
{
return { 1, 2, 3 }; // error: can't deduce type for { 1, 2, 3 }
}
  1. lambdas may use auto in parameter declarations.
1
2
3
4
5
6
std::vector<int> v;

auto resetV =
[&v](const auto& newValue) { v = newValue; }; // C++14

resetV({ 1, 2, 3 }); // error! can't deduce type for { 1, 2, 3 }

auto 的 2 个使用的意义: template type deductionauto type deduction, C++ 11 中的为后者, C++ 14 中增加了前者.

Things to Remember

  • auto type deduction is usually the same as template type deduction, but auto type deduction assumes that a braced initializer represents a std::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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const int i = 0; // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) is int
}; // decltype(Point::y) is int

Widget w; // decltype(w) is Widget

if (f(w)) … // decltype(f(w)) is bool

template<typename T> // simplified version of std::vector
class vector {
public:

T& operator[](std::size_t index);

};

vector<int> v; // decltype(v) is vector<int>

if (v[0] == 0) … // decltype(v[0]) is int&

trailing return type

C++11

1
2
3
4
5
6
7
template<typename Container, typename Index> // works, but requires refinement
auto authAndAccess(Container& c, Index i)
-> decltype(c[i])
{
authenticateUser();
return c[i];//`operator[]` on a container of objects of type `T` typically returns a `T&`.
}

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
2
3
4
5
6
7
8
9
10
11
template<typename Container, typename Index> // C++14; not quite correct
auto authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i]; // return type deduced from c[i]
}

std::deque<int> d;

authAndAccess(d, 5) = 10; // authenticate user, return d[5],then assign 10 to it;
// this won't compile!

无法编译的解释: 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
2
3
4
5
6
7
template<typename Container, typename Index> // C++14; works, but still requires refinement
decltype(auto)
authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];
}

为什么会是这种形式 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
2
3
4
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction: myWidget1's type is Widget
decltype(auto) myWidget2 = cw; // decltype type deduction: myWidget2's type is const Widget&

refinement with respect to rvalue reference

1
2
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

上面的参数是 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
2
3
4
std::deque<std::string> makeStringDeque(); // factory function
// make copy of 5th element of deque returned
// from makeStringDeque
auto s = authAndAccess(makeStringDeque(), 5);

虽然可以通过 overload 分别应用到左右值引用. 更推荐 universal reference

1
2
template<typename Container, typename Index> // c is now a
decltype(auto) authAndAccess(Container&& c, Index i); // universal reference

PS. 这里作者很严谨地进行了下面的探讨:
对于 Index i 参数作者虽然不推荐使用 pass-by-value, 但是考虑到 STL 中的下标一般都是 int 类似的, 可以使用 pass-by-value(当然也存在 std::map 的 key 是比较大的类型的情况).

加上完美转发 std::forward:

1
2
3
4
5
6
7
template<typename Container, typename Index> // final C++14 version
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}

对应的 C++11 版本:

1
2
3
4
5
6
7
template<typename Container, typename Index> // final C++11 version
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}

对非 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 affect decltype’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
2
3
4
5
6
7
8
9
10
11
12
13
14
decltype(auto) f1()
{
int x = 0;

return x; // decltype(x) is int, so f1 returns int
}

decltype(auto) f2()
{
int x = 0;

return (x); // decltype((x)) is int&, so f2 returns int&
//dangling!
}

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 of T&.
  • C++14 supports decltype(auto), which, like auto, deduces a type from its initializer, but it performs the type deduction using the decltype 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
2
3
4
template<typename T> // declaration only for TD;
class TD; // TD == "Type Displayer"
TD<decltype(x)> xType; // elicit errors containing
TD<decltype(y)> yType; // x's and y's types

编译一定会报错: because there’s no template definition to instantiate.

一种报错输出:

1
2
error: aggregate 'TD<int> xType' has incomplete type and cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined

可以看到 'TD<int> xType' 的部分把编译器推断的结果打印了出来.

Runtime Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T> // template function to be called
void f(const T& param);

std::vector<Widget> createVec(); // factory function init vw w/factory return
const auto vw = createVec();

if (!vw.empty()) {
f(&vw[0]); // call f

}

//打印类型
template<typename T>
void f(const T& param)
{
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; // show T
cout << "param = " << typeid(param).name() << '\n'; // show param's type

}

GNU and Clang compilers produce this output:

1
2
T = PK6Widget
param = 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
2
T = class Widget const *
param = 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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
// show T
cout << "T = " << type_id_with_cvr<T>().pretty_name()
<< '\n';
// show param's type
cout << "param = " << type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';

}
  • 语源
    with_cvr : it doesn’t remove const, volatile, or reference qualifiers.

pretty_name member function produces a std::string containing a human-friendly representation of the type.

Boost TypeIndex 的输出:

1
2
T = Widget const*
param = Widget const* 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
2
3
int x1; // potentially uninitialized
auto x2; // error! initializer required
auto x3 = 0; // fine, x's value is well-defined
  • verbose variable declarations(简洁, 节省打字时间)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename It> // algorithm to dwim ("do what I mean")
void dwim(It b, It e) // for all elements in range from b to e
{
while (b != e) {
typename std::iterator_traits<It>::value_type
currValue = *b;
…}
}

template<typename It> // as before
void dwim(It b, It e)
{
while (b != e) {
auto currValue = *b;
…}
}
  • the ability to directly hold closures(比 std::function 很多时候有优势)
1
2
3
4
5
6
7
8
9
10
11
// std::function without using auto
std::function<bool(const std::unique_ptr<Widget> &,
const std::unique_ptr<Widget> &)>
derefUPLess = [](const std::unique_ptr<Widget> &p1,
const std::unique_ptr<Widget> &p2)
{ return *p1 < *p2; };
//lambda with auto
auto derefLess = // C++14 comparison
[](const auto& p1, // function for
const auto& p2) // values pointed
{ return *p1 < *p2; }; // to by anything pointer-like

lambda with auto 的优势(std::function 的劣势):

  1. 内存使用较大: std::function has a fixed size for any given signature. This size may not be adequate for the closure it’s asked to store.
  2. 较慢: 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 a std::function object is almost certain to be slower than calling it via an auto-declared object.
  3. Use lambdas instead of std::bind in Item 34.
  • “type shortcuts”

例子1:

1
2
3
4
std::vector<int> v;

unsigned sz = v.size();//maybe not portable
auto sz = v.size(); // sz's type is std::vector<int>::size_type

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
2
3
4
5
6
7
8
9
10
11
std::unordered_map<std::string, int> m;

for (const std::pair<std::string, int>& p : m)//inefficient!
{
// do something with p
}

for (const auto& p : m) // no temporary object
{
// as before
}

问题: 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 of auto.

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 that operator[] for std::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+ for Matrix objects returns a proxy for the result instead of the result itself. 即 a proxy class such as Sum<Matrix, Matrix> instead of a Matrix object. encode the entire initialization expression be something like Sum<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
2
3
4
5
6
std::vector<bool> features(const Widget& w);
Widget w;

bool highPriority = features(w)[5]; // is w high priority?

processWidget(w, highPriority); // process w in accord with its priority

使用 auto:

1
2
auto highPriority = features(w)[5]; // is w high priority?
processWidget(w, highPriority); // undefined behavior!

出错原因: 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?

  1. documentation
  2. header files: Paying careful attention to the interfaces you’re using can often reveal the existence of proxy classes. 例子如下:
1
2
3
4
5
6
7
8
9
10
namespace std { // from C++ Standards
template <class Allocator>
class vector<bool, Allocator> {
public:

class reference { … };
reference operator[](size_type n); //警觉此处非普通返回, 而是返回了一个对象

};
}

怎么解决这个问题

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
2
auto highPriority = static_cast<bool>(features(w)[5]);
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

explicitly typed initializer idiom 别的用处–方便别人理解意图

例子 1: 显示地说明我进行了强制转换:

1
2
3
double calcEpsilon(); // return tolerance value
float ep = calcEpsilon(); // impliclitly convert double → float
auto ep = static_cast<float>(calcEpsilon());//显式地告诉阅读代码的人, 故意把 double 转换为了 float

例子 2: 使用小数作为容器下标的例子:

1
2
int index = d * c.size();
auto index = static_cast<int>(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
2
3
4
int x(0); // initializer is in parentheses
int y = 0; // initializer follows "="
int z{ 0 }; // initializer is in braces
int z = { 0 }; // initializer uses "=" and braces. the same as the braces-only version.

注意: 误解 equals 不一定是 assignment 如下:

1
2
3
Widget w1; // call default constructor
Widget w2 = w1; // not an assignment; calls copy ctor
w1 = w2; // an assignment; calls copy operator=

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.

  1. formerly inexpressible sovled
1
std::vector<int> v{ 1, 3, 5 }; // v's initial content is 1, 3, 5
  1. specify default initialization values for non-static data members.
1
2
3
4
5
6
7
class Widget {

private:
int x{ 0 }; // fine, x's default value is 0
int y = 0; // also fine
int z(0); // error!
};
  1. uncopyable objects (e.g., std::atomics—see Item 40) may be initialized using braces or parentheses, but not using =:
1
2
3
std::atomic<int> ai1{ 0 }; // fine
std::atomic<int> ai2(0); // fine
std::atomic<int> ai3 = 0; // error!
  1. 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
2
3
4
5
6
7
double x, y, z;

int sum1{ x + y + z }; // error! sum of doubles may not be expressible as int

//However
int sum2(x + y + z); // okay (value of expression truncated to an int)
int sum3 = x + y + z; // ditto
  1. 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
2
3
4
Widget w1(10); // call Widget ctor with argument 10
Widget w2(); // most vexing parse! declares a function
// named w2 that returns a Widget!
Widget w3{}; // calls Widget ctor with no args

drawback to braced initialization

  1. Item 2 中提到的 auto 与之不兼容的问题.
  2. 破坏构造函数中的 overload resolution, 强制带 std::initializer_list 的重载为最优先, 即便是通过 implicit conversion 也要换成 std::initializer_list 导致很多违反直觉的结果.

当不存在 std::initializer_list 的重载构造函数时:

1
2
3
4
5
6
7
8
9
10
11
class Widget {
public:
Widget(int i, bool b); // ctors not declaring
Widget(int i, double d); // std::initializer_list params

};

Widget w1(10, true); // calls first ctor
Widget w2{10, true}; // also calls first ctor
Widget w3(10, 5.0); // calls second ctor
Widget w4{10, 5.0}; // also calls second ctor

当存在 std::initializer_list 的重载构造函数时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<long double> il); // added

};

Widget w1(10, true); // uses parens and, as before,
// calls first ctor
Widget w2{10, true}; // uses braces, but now calls
// std::initializer_list ctor
// (10 and true convert to long double)
Widget w3(10, 5.0); // uses parens and, as before,
// calls second ctor
Widget w4{10, 5.0}; // uses braces, but now calls
// std::initializer_list ctor
// (10 and 5.0 convert to long double)

copy and move construction can be hijacked by std::initializer_list constructors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<long double> il); // as before
operator float() const; // convert to float

};
Widget w5(w4); // uses parens, calls copy ctor
Widget w6{w4}; // uses braces, calls std::initializer_list ctor
// (w4 converts to float, and float converts to long double)
Widget w7(std::move(w4)); // uses parens, calls move ctor
Widget w8{std::move(w4)}; // uses braces, calls std::initializer_list ctor
// (for same reason as w6)

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
2
3
4
5
6
7
8
9
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<bool> il); // element type is now bool
// no implicit conversion funcs
};

Widget w{10, 5.0}; // error! requires narrowing conversions

优先级之高导致: 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
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
// std::initializer_list element type is now std::string
Widget(std::initializer_list<std::string> il);
// no implicit conversion funcs
};

Widget w1(10, true); // uses parens, still calls first ctor
Widget w2{10, true}; // uses braces, now calls first ctor
Widget w3(10, 5.0); // uses parens, still calls second ctor
Widget w4{10, 5.0}; // uses braces, now calls second ctor

edge case: “no arguments” std::initializer_list constructor VS default constructor

Empty braces mean no arguments, not an empty std::initializer_list:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
public:
Widget(); // default ctor
Widget(std::initializer_list<int> il); // std::initializer_list ctor
// no implicit conversion funcs
};

Widget w1; // calls default ctor
Widget w2{}; // also calls default ctor
Widget w3(); // most vexing parse! declares a function!
//如果想表达 empty std::initializer_list
Widget w4({}); // calls std::initializer_list ctor with empty list
Widget w5{{}}; // ditto

实际 std::initializer_list constructor 应用的例子:std::vector

1
2
3
4
std::vector<int> v1(10, 20); // use non-std::initializer_list ctor: create 10-element
// std::vector, all elements have value of 20
std::vector<int> v2{10, 20}; // use std::initializer_list ctor: create 2-element std::vector,
// element values are 10 and 20

对于开发者的建议

  1. 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 the std::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.

  1. 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. () 还是{} 为主都可以.

  2. 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
2
3
4
5
6
7
template<typename T, // type of object to create
typename... Ts> // types of arguments to use
void doSomeWork(Ts&&... params)
{
create local T object from params...

}

There are two ways to turn the line of pseudocode into real code:

1
2
3
4
5
6
T localObject(std::forward<Ts>(params)...); // using parens
T localObject{std::forward<Ts>(params)...}; // using braces

std::vector<int> v;

doSomeWork<std::vector<int>>(10, 20);

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
2
3
4
5
6
void f(int); // three overloads of f
void f(bool);
void f(void*);
f(0); // calls f(int), not f(void*)
f(NULL); // might not compile, but typically calls
// f(int). Never calls f(void*)

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.
nullptr’s actual type is std::nullptr_t.
The type std::nullptr_timplicitly 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
2
3
4
5
6
7
8
9
10
auto result = findRecord( /* arguments */ );
// pointer or integral?
if (result == 0) {

}

//there’s no ambiguity: result must be a pointer type.
if (result == nullptr) {

}

用处2: template type deduction deduces the “wrong” types for 0 and NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//函数声明
int f1(std::shared_ptr<Widget> spw); // call these only when
double f2(std::unique_ptr<Widget> upw); // the appropriate
bool f3(Widget* pw); // mutex is locked

//应用模板前的做法, 0 NULL 还算能工作
std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxGuard = // C++11 typedef; see Item 9
std::lock_guard<std::mutex>;


{
MuxGuard g(f1m); // lock mutex for f1
auto result = f1(0); // pass 0 as null ptr to f1 unlock mutex
}


{
MuxGuard g(f2m); // lock mutex for f2
auto result = f2(NULL); // pass NULL as null ptr to f2 unlock mutex
}


{

MuxGuard g(f3m); // lock mutex for f3
auto result = f3(nullptr); // pass nullptr as null ptr to f3 unlock mutex
}

// 换成模板
template<typename FuncType,
typename MuxType,
typename PtrType>
auto lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr) -> decltype(func(ptr))
{
MuxGuard g(mutex);
return func(ptr);
}

auto result1 = lockAndCall(f1, f1m, 0); // error!

auto result2 = lockAndCall(f2, f2m, NULL); // error!

auto result3 = lockAndCall(f3, f3m, nullptr); // fine

0 与 NULL 出现 error 的原因是编译器会优先把它们推断成 int(long) 型与函数 f1 f2 声明的智能指针不匹配.

Things to Remember

  • Prefer nullptr to 0 and NULL.
  • Avoid overloading on integral and pointer types.

Item 9: Prefer alias declarations to typedefs.

一般场景下区别不明显:

1
2
3
4
5
6
7
8
9
10
11
typedef
std::unique_ptr<std::unordered_map<std::string, std::string>>
UPtrMapSS;
using UPtrMapSS =
std::unique_ptr<std::unordered_map<std::string, std::string>>;
//函数指针
// FP is a synonym for a pointer to a function taking an int and
// a const std::string& and returning nothing
typedef void (*FP)(int, const std::string&); // typedef
//same meaning as above
using FP = void (*)(int, const std::string&); // alias declaration

区别在模板使用上:

In particular, alias declarations may be templatized (in which case they’re called alias templates), while typedefs cannot.

  • typedefs nested inside templatized structs. 需要手动把模板推导功能再来一遍.
1
2
3
4
5
6
7
8
9
10
11
12
template<typename T> 
using MyAllocList = std::list<T, MyAlloc<T>>;

MyAllocList<Widget> lw;

//typedef
template<typename T>
struct MyAllocList {
typedef std::list<T, MyAlloc<T>> type;
};

MyAllocList<Widget>::type lw;
  • dependent type: must be preceded by typename. 使用 alias 会使得 MyAllocList<T> is a non-dependent type, 确定一定是类型而不是成员变量, 因此不需要 typename.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class Widget { // Widget<T> contains a MyAllocList<T>
private:
typename MyAllocList<T>::type list; //as a data member

};

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; //as before
template<typename T>
class Widget {
private:
MyAllocList<T> list; // no "typename", no "::type"

};

来自 Standardization Committee 的背书

C++11 gives you the tools to perform type transformations in the form of type traits(<type_traits>).

1
2
3
std::remove_const<T>::type // yields T from const T
std::remove_reference<T>::type // yields T from T& and T&&
std::add_lvalue_reference<T>::type // yields T& from T

C++11 由于历史原因没有使用 alias template. type traits are implemented as nested typedefs 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
2
3
4
5
6
std::remove_const<T>::type // C++11: const T → T
std::remove_const_t<T> // C++14 equivalent
std::remove_reference<T>::type // C++11: T&/T&& → T
std::remove_reference_t<T> // C++14 equivalent
std::add_lvalue_reference<T>::type // C++11: T → T&
std::add_lvalue_reference_t<T> // C++14 equivalent

在 C++ 14 中实现 std::transformation_t 所用的 alias template.

1
2
3
4
5
6
7
8
9
template <class T>
using remove_const_t = typename remove_const<T>::type;

template <class T>
using remove_reference_t = typename remove_reference<T>::type;

template <class T>
using add_lvalue_reference_t =
typename add_lvalue_reference<T>::type;

Things to Remember

  • typedefs don’t support templatization, but alias declarations do.
  • Alias templates avoid the ::type suffix and, in templates, the typename prefix often required to refer to typedefs.
  • C++14 offers alias templates for all the C++11 type traits transformations.

Item 10: Prefer scoped enums to unscoped enums.

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 中的例外: enums. 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
2
enum Color { black, white, red }; // black, white, red are in same scope as Color
auto white = false; // error! white already declared in this scope

这种例外有专用的名称: enum definition gives rise to the official term for this kind of enum: unscoped.

C++11 中有 scoped enums, they’re sometimes referred to as enum classes.

1
2
3
4
5
enum class Color { black, white, red }; // black, white, red are scoped to Color
auto white = false; // fine, no other "white" in scope
Color c = white; // error! no enumerator named "white" is in this scope
Color c = Color::white; // fine
auto c = Color::white; // also fine (and in accord with Item 5's advice)

应用: implicit conversions to int

1
2
3
4
5
6
7
8
9
10
enum Color { black, white, red }; // unscoped enum
std::vector<std::size_t> // func. returning
primeFactors(std::size_t x); // prime factors of x
Color c = red;

if (c < 14.5) { // compare Color to double (!)
auto factors = // compute prime factors of a Color (!)
primeFactors(c);

}

no implicit conversions from enumerators in a scoped enum to any other type:

1
2
3
4
5
6
7
8
enum class Color { black, white, red }; // enum is now scoped
Color c = Color::red; // as before, but with scope qualifier

if (c < 14.5) { // error! can't compare Color and double
auto factors = // error! can't pass Color to
primeFactors(c); // function expecting std::size_t

}

如果真的想转换:

1
2
3
4
5
if (static_cast<double>(c) < 14.5) { // odd code, but it's valid
auto factors = // suspect, but it compiles
primeFactors(static_cast<std::size_t>(c));

}

scoped enums may be forward-declared:

1
2
enum Color; // error!
enum class Color; // fine

背后的原理是怎样的呢?

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
2
3
4
5
6
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};

If a new status value is then introduced,

1
2
3
4
5
6
7
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,// new status value
indeterminate = 0xFFFFFFFF
};

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 enums 不会, 只会在使用新加 value 地方需要重新编译.

1
2
enum class Status; // forward declaration
void continueProcessing(Status s); // use of fwd-declared enum

那 scoped enums 是怎么做到这一点的呢?

The answer is simple: the underlying type for a scoped enum is always known, and for unscoped enums, you can specify it.

By default, the underlying type for scoped enums is int. If the default doesn’t suit you, you can override it.

1
2
3
4
5
6
7
8
9
10
11
enum class Status; // underlying type is int DEFAULT
enum class Status: std::uint32_t; // underlying type for Status is std::uint32_t (from <cstdint>)

//Underlying type specifications can also go on an enum’s definition
enum class Status: std::uint32_t { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};

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 enums 的用武之地的 scope enums 化

there’s at least one situation where unscoped enums may be useful. That’s when referring to fields within C++11’s std::tuples.

1
2
3
4
5
6
7
8
using UserInfo = 
std::tuple<std::string, // name
std::string, // email
std::size_t> ; // reputation

UserInfo uInfo; // object of tuple type

auto val = std::get<1>(uInfo); // get value of field 1

谁能记得住 UserInfo 的第一字段的具体内容, 这种场景还是 unscoped enums 比较好用:

1
2
3
4
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo; // as before

auto val = std::get<uiEmail>(uInfo); // ah, get value of email field

使用 scoped enums 没有那么简洁(substantially more verbose):

1
2
3
4
5
6
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo; // as before

auto val =
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>
(uInfo);

那要如何改进 scope enum 不用这么复杂呢?

这依赖于下面三个想法:

  1. return the enum’s underlying type via the std::underlying_type type trait.
  2. use constexpr to produce its result during compilation.
  3. it will never yield an exception –> declare it noexcept.

C++11 下对 scope enum 的改进

1
2
3
4
5
6
7
8
template<typename E>
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return
static_cast<typename
std::underlying_type<E>::type>(enumerator);
}

C++14 下使用 std::underlying_type_t

1
2
3
4
5
6
template<typename E> // C++14
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

C++14 进一步使用 auto:

1
2
3
4
5
6
template<typename E> // C++14
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

使用改造后 scoped enum 的例子:

1
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

Things to Remember

  • C++98-style enums are now known as unscoped enums.
  • Enumerators of scoped enums are visible only within the enum. They convert to other types only with a cast.
  • Both scoped and unscoped enums support specification of the underlying type. The default underlying type for scoped enums is int. Unscoped enums have no default underlying type.
  • Scoped enums may always be forward-declared. Unscoped enums may be forward-declared only if their declaration specifies an underlying type.

Item 10: Prefer scoped enums to unscoped enums.

namespace pollution

C++ 98 下的 enum 与常规的 {} scope 规则相违背, 导致 namespace pollution ==> unscoped enums.
C++ 11 新加了 scoped enums.

具体描述: 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 enums. 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
2
3
4
5
6
7
enum Color { black, white, red };
auto white = false;// error! white already declared in this scope
enum class Color { black, white, red };
auto white = false; // fine, no other
Color c = white;// error! no enumerator named "white" is in this scope
Color c = Color::white;// fine
auto c = Color::white;// also fine (and in accord with Item 5's advice)

nonsensical implicit type conversions

由于 unscoped enums 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
enum Color// unscoped enum
{
black,
white,
red
};
std::vector<std::size_t> primeFactors(std::size_tx);
Color c = red;
if (c < 14.5)
{
// compare Color to double (!)
auto factors = primeFactors(c);
}
enum class Color// enum is now scoped
{
black,
white,
red
};

Color c = Color::red;
if (c < 14.5) // error! can't compare Color and double
{
auto factors = primeFactors(c);
// error! can't pass Color to function expecting std::size_t
}

//正确的用法
if (static_cast<double>(c) < 14.5)
{
// odd code, but it's valid
auto factors = primeFactors(static_cast<std::size_t>(c));
...
}

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
2
3
4
5
6
7
8
9
enum Status
{
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,//newly added
indeterminate = 0xFFFFFFFF
};

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
2
enum class Status; // forward declaration
void continueProcessing(Status s); // use of fwd-declared enum

指定 underlying type 以实现 forward-declaration.

1
2
3
4
5
6
7
8
9
10
enum Color : std::uint8_t;// fwd decl for unscoped enum underlying type is std::uint8_t
enum class Status : std::uint32_t
{
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};

drawback

有些时候 unscoped enum 的 implicit conversion 还是比较方便的, 换成 scoped enum 可能没那么方便. trade-off: 利便性 VS 正确性.

例如 std::tupleget<> 模板函数的访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UserInfo =
std::tuple<std::string,
std::string,
std::size_t>;
UserInfo uInfo;// object of tuple type
...
auto val = std::get<1>(uInfo);// get value of field 1

//with unscoped enums
enum UserInfoFields
{
uiName,
uiEmail,
uiReputation
};
UserInfo uInfo;// as before
auto val = std::get<uiEmail>(uInfo);// ah, get value of email field

切换为 scoped enum

1
2
3
4
5
6
7
8
9
10
// scoped enums is substantially more verbose:
enum class UserInfoFields
{
uiName,
uiEmail,
uiReputation
};
UserInfo... uInfo;// as before
//it must be a constexpr function.
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename E> // c++11
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return static_cast<typename std::underlying_type<E>::type>(enumerator);
}

template <typename E> // C++14
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

// usage
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

Things to Remember

  • C++98-style enums are now known as unscoped enums.
  • Enumerators of scoped enums are visible only within the enum. They convert to other types only with a cast.
  • Both scoped and unscoped enums support specification of the underlying type. The default underlying type for scoped enums is int. Unscoped enums have no default underlying type.
  • Scoped enums may always be forward-declared. Unscoped enums may be forward-declared only if their declaration specifies an underlying type.

Item 11: Prefer deleted functions to private undefined ones.

C++98 的做法: 声明为 private 并且不提供定义:

1
2
3
4
5
6
7
8
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:

private:
basic_ios(const basic_ios& ); // not defined
basic_ios& operator=(const basic_ios&); // not defined
};

实现的效果有2点:

  1. Declaring these functions private prevents clients from calling them.
  2. member functions or friends of the classuses them, linking will fail due to missing function definitions.

C++11 使用 delete, 将其改造为 deleted functions.

1
2
3
4
5
6
7
8
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:

basic_ios(const basic_ios& ) = delete;
basic_ios& operator=(const basic_ios&) = delete;

};

与 C++98 做法相比有如下改进:

  1. member and friend functions will fail to compile.
  2. deleted functions are declared public, not private. 原因是 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
2
3
4
5
6
7
8
9
bool isLucky(int number);
if (isLucky('a')) … // is 'a' a lucky number?
if (isLucky(true)) … // is "true"?
if (isLucky(3.5)) … // should we truncate to 3 before checking for luckiness?

bool isLucky(int number); // original function
bool isLucky(char) = delete; // reject chars
bool isLucky(bool) = delete; // reject bools
bool isLucky(double) = delete; // reject doubles and floats

这种做法的细节:

  1. overload is deleted will prevent the call from compiling.
  2. 利用 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
2
3
4
5
6
7
8
9
10
template<typename T>
void processPointer(T* ptr);
template<>
void processPointer<void>(void*) = delete;
template<>
void processPointer<char>(char*) = delete;
template<>
void processPointer<const void>(const void*) = delete;
template<>
void processPointer<const char>(const char*) = delete;

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
2
3
4
5
6
7
8
9
10
class Widget {
public:

template<typename T>
void processPointer(T* ptr)
{ … }
private:
template<> // error!would not compile
void processPointer<void>(void*);
};

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
2
3
4
5
6
7
8
9
10
class Widget {
public:

template<typename T>
void processPointer(T* ptr)
{ … }

};
template<> // still public, but deleted
void Widget::processPointer<void>(void*) = delete;

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
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
public:

void doWork() &; // this version of doWork applies only when *this is an lvalue
void doWork() &&; // this version of doWork applies only when *this is an rvalue
};


Widget makeWidget(); // factory function (returns rvalue)
Widget w; // normal object (an lvalue)

w.doWork(); // calls Widget::doWork for lvalues (i.e., Widget::doWork &)
makeWidget().doWork(); // calls Widget::doWork for rvalues (i.e., Widget::doWork &&)

编译器提示

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const;
};

//不符合 override 规则
//elicit compiler warnings or not, 不可依赖
class Derived : public Base
{
public:
virtual void mf1();
virtual void mf2(unsigned int x);
virtual void mf3() &&;
void mf4() const;
};

//make explicit that a derived class function is
//supposed to override a base class version
//won’t compile
//给了编译器提示, 因此可以发现违反本意的错误.
class Derived : public Base
{
public:
virtual void mf1() override;
virtual void mf2(unsigned int x) override;
virtual void mf3() && override;
virtual void mf4() const override;
};

//OK case
class Derived: public Base {
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
void mf4() const override; // adding "virtual" is OK, but not necessary
};

方便测试

利用 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
2
3
4
5
6
class Warning { // potential legacy class from C++98
public:

void override(); // legal in both C++98 and C++11 (with the same meaning)

};

more about member function reference qualifiers

例如提供数据给 client 修改, 先是左值引用的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
public:
using DataType = std::vector<double>;

DataType& data() { return values; }

private:
DataType values;
};

Widget w;

auto vals1 = w.data(); // copy w.values into vals1

如果是右值引用的情况下:

1
2
Widget makeWidget();
auto vals2 = makeWidget().data(); // copy values inside the Widget into vals2

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Widget {
public:
using DataType = std::vector<double>;

DataType& data() & // for lvalue Widgets, return lvalue
{ return values; }
DataType data() && // for rvalue Widgets, return rvalue
{ return std::move(values); }

private:
DataType values;
};

auto vals1 = w.data(); // calls lvalue overload for Widget::data, copy-constructs vals1
auto vals2 = makeWidget().data(); // calls rvalue overload for Widget::data, move-constructs vals2

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_iterators to iterators.

const_iterators are the STL equivalent of pointers-to-const.

C++98 里为啥 const_iterators 不受欢迎?

  1. 获取困难: there was no simple way to get a const_iterator from a non-const container.
    虽然也有别的方法实现这种转换, (e.g., you could bind values to a reference-to-const variable, then use that variable in place of values in your code).
    下面是通过 casting 的形式实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 不使用 const_iterator
std::vector<int> values;


std::vector<int>::iterator it =
std::find(values.begin(),values.end(), 1983);

values.insert(it, 1998);

//想要使用 const_iterator
typedef std::vector<int>::iterator IterT;
typedef std::vector<int>::const_iterator ConstIterT;
std::vector<int> values;


ConstIterT ci =
std::find(static_cast<ConstIterT>(values.begin()), // cast
static_cast<ConstIterT>(values.end()), // cast
1983);

values.insert(static_cast<IterT>(ci), 1998); // may not compile; see below
  1. const_iterators 到 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_iterators simply don’t convert to iterators, no matter how much it might seem like they should.

C++11 的进步

  1. 获取变简单了: The container member functions cbegin and cend produce const_iterators.
  2. 使用场景被拓宽了: even for non-const containers, and STL member functions that use iterators to identify positions (e.g., insert and erase) actually use const_iterators.
1
2
3
4
5
std::vector<int> values; // as before

auto it =
std::find(values.cbegin(),values.cend(), 1983); // use cbegin and cend
values.insert(it, 1998);//不用转换了

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
2
3
4
5
template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{
return std::begin(container); // see explanation below
}

注意: 为什么 return std::begin() 而不是 return std::cbegin()?

  • 很多 containerstd::begin() 没有 std::cbegin().
  • 能够通过推导实现: Invoking the non-member begin function (provided by C++11) on a const container yields a const_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
2
3
4
5
6
7
8
9
10
11
12
13
14
// in container, find first occurrence of targetVal, then insert insertVal there
template<typename C, typename V>
void findAndInsert(C& container,
const V& targetVal,
const V& insertVal)
{
using std::cbegin;
using std::cend;
auto it = std::find(cbegin(container), // non-member cbegin
cend(container), // non-member cend
targetVal);

container.insert(it, insertVal);
}

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
2
int f(int x) throw();// no exceptions from f: C++98 style
int f(int x) noexcept;// no exceptions from f: C++11 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
2
3
RetType function(params) noexcept; // most optimizable
RetType function(params) throw(); // less optimizable
RetType function(params); // less optimizable

改进的好处 3: 帮助明确可以使用 move semantics

很多 STL 算法提供了对 move semantics 的支持, 但是考虑到异常相关的安全问题, 采用的策略是 “move if you can, but copy if you must” 例如 std::vector::push_back.

所谓的异常相关的安全问题拆解如下:

1
2
3
4
5
6
std::vector<Widget> vw;
...
Widget w; // work with w
...
vw.push_back(w); // add w to 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
2
3
4
5
6
7
8
9
10
11
template <class T, size_t N>
void swap(T (&a)[N],
T (&b)[N]) noexcept(noexcept(swap(*a, *b)));

template <class T1, class T2>
struct pair {

void swap(pair& p) noexcept(noexcept(swap(first, p.first)) &&
noexcept(swap(second, p.second)));

};

可以看到出现了 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 的讨论

  1. 添加/去除 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.

  1. 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.

  1. 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) 如果硬要声明一个析构函数可以抛出异常.

  1. 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. 编译器对异常使用与否的判断无能为力
1
2
3
4
5
6
7
8
void setup(); // functions defined elsewhere
void cleanup();
void doWork() noexcept
{
setup(); // set up work to be done do the actual work

cleanup(); // perform cleanup actions
}

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
2
3
4
5
6
int sz; // non-constexpr variable

constexpr auto arraySize1 = sz; // error! sz's value not known at compilation
std::array<int, sz> data1; // error! same problem
constexpr auto arraySize2 = 10; // fine, 10 is a compile-time constant
std::array<int, arraySize2> data2; // fine, arraySize2 is constexpr

constconstexpr

const 无法胜任 constexpr : const objects need not be initialized with values known during compilation.

1
2
3
4
int sz; // as before

const auto arraySize = sz; // fine, arraySize is const copy of sz
std::array<int, arraySize> data; // error! arraySize's value not known at compilation

All constexpr objects are const, but not all const objects are constexpr.

constexpr functions 的双面性

  1. 编译期: constexpr functions can be used in contexts that demand compile-time constants. If the values of the arguments you pass to a constexpr function in such a context are known during compilation, the result will be computed during compilation.
  2. 运行期: 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. 三目运算符
  2. 递归
1
2
3
4
constexpr int pow(int base, int exp) noexcept
{
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}

C++14 取消了这个限制:

1
2
3
4
5
6
constexpr int pow(int base, int exp) noexcept // C++14
{
auto result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
public:
constexpr Point(double xVal = 0, double yVal = 0) noexcept
: x(xVal), y(yVal)
{}
constexpr double xValue() const noexcept { return x; }//can also be constexpr
constexpr double yValue() const noexcept { return y; }//can also be constexpr
void setX(double newX) noexcept { x = newX; }
void setY(double newY) noexcept { y = newY; }
private:
double x, y;
};

// Points initialized could thus be constexpr:
constexpr Point p1(9.4, 27.7); // fine, "runs" constexpr ctor during compilation
constexpr Point p2(28.8, 5.3); // also fine

//成员函数
constexpr Point midpoint(const Point &p1, const Point &p2) noexcept
{
return {(p1.xValue() + p2.xValue()) / 2,// call constexpr
(p1.yValue() + p2.yValue()) / 2}; // member funcs
}
constexpr auto mid = midpoint(p1, p2);// init constexpr object w/result of constexpr function

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 在这上面的缺陷:

  1. they modify the object they operate on, and in C++11, constexpr member functions are implicitly const.
  2. they have voidreturn types, and void isn’t a literal type in C++11.

C++14 解除了这些限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Point {
public:

constexpr void setX(double newX) noexcept // C++14
{ x = newX; }
constexpr void setY(double newY) noexcept // C++14
{ y = newY; }

};

// 利用 set 函数制造 non-member function
// return reflection of p with respect to the origin (C++14)
constexpr Point reflection(const Point& p) noexcept
{
Point result; // create non-const Point
result.setX(-p.xValue()); // set its x and y values
result.setY(-p.yValue());
return result; // return copy of it
}
//Client code could look like this
constexpr Point p1(9.4, 27.7);
constexpr Point p2(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2);
constexpr auto reflectedMid = // reflectedMid's value is (-19.1 -16.5) and known during compilation
reflection(mid);

Things to Remember

  • constexpr objects are const 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 than non-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Polynomial {
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
if (!rootsAreValid) { // if cache not valid compute roots,
//store them in rootVals
rootsAreValid = true;
}
return rootVals;
}

private:
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};

roots() 虽然被声明为 const 但是由于它需要去第一次计算的时候更改 rootVals, 因此把两个 member data 都设置成了 mutable.

多线程使用 root() 可能出现 data race.

1
2
3
4
Polynomial p;

/*----- Thread 1 ----- */ /*------- Thread 2 ------- */
auto rootsOfP = p.roots(); auto valsGivingZero = p.roots();

The problem is that roots is declared const, but it’s not thread safe.

解决办法有 2:

  1. std::mutex
  2. std::atomic

先是第一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Polynomial {
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
std::lock_guard<std::mutex> g(m); // lock mutex if cache not valid compute/store roots
if (!rootsAreValid) {

rootsAreValid = true;
}
return rootVals;
} // unlock mutex
private:
mutable std::mutex m;
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};

第二个的例子, 计算对象函数的计算次数:

1
2
3
4
5
6
7
8
9
10
11
12
class Point { // 2D point
public:

double distanceFromOrigin() const noexcept
{
++callCount; // atomic increment
return std::sqrt((x * x) + (y * y));
}
private:
mutable std::atomic<unsigned> callCount{ 0 };
double x, y;
};

std::mutexstd::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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Widget {
public:

int magicValue() const
{
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2; // uh oh, part 1
cacheValid = true; // uh oh, part 2
return cachedValue;
}
}
private:
mutable std::atomic<bool> cacheValid{ false };
mutable std::atomic<int> cachedValue;
};

This will work, but sometimes it will work a lot harder than it should.

  • A thread calls Widget::magicValue, sees cacheValid as false, performs the two expensive computations, and assigns their sum to cachedValue.
  • At that point, a second thread calls Widget::magicValue, also sees cacheValid 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Widget {
public:

int magicValue() const
{
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValid = true; // uh oh, part 1
return cachedValue = val1 + val2; // uh oh, part 2
}
}

};

即调换 part 1 与 part 2 的顺序. 但是这么做会导致错误:

  • One thread calls Widget::magicValue and executes through the point where cacheValid is set to true.
  • At that moment, a second thread calls Widget::magicValue and checks cacheValid. Seeing it true, the thread returns cachedValue, even though the first thread has not yet made an assignment to it. The returned value is therefore incorrect.

应该使用 std::mutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Widget {
public:

int magicValue() const
{
std::lock_guard<std::mutex> guard(m); // lock m
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
} // unlock m

private:
mutable std::mutex m;
mutable int cachedValue; // no longer atomic
mutable bool cacheValid{ false }; // no longer atomic
};

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
2
3
4
5
6
7
class Widget {
public:

Widget(Widget&& rhs); // move constructor
Widget& operator=(Widget&& rhs); // move assignment operator

};

move operation 机理

  1. Move operations perform “memberwise moves” on the non-static data members of the class.

  2. The move constructor also move-constructs its base class parts (if there are any).

  3. 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.

  4. 更深层一些 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
2
3
4
5
6
7
8
9
class Widget {
public:

~Widget(); // user-declared dtor default copy ctor

Widget(const Widget&) = default; // default copy ctor behavior is OK
Widget& operator=(const Widget&) = default; // default copy assign behavior is OK

};

什么情况下比较常用?

  1. polymorphic base classes => virtual destructors

Often, the default implementation would be correct, and = default is a good way to express that.

1
2
3
4
5
6
7
8
9
class Base {
public:
virtual ~Base() = default; // make dtor virtual
Base(Base&&) = default; // support moving
Base& operator=(Base&&) = default;
Base(const Base&) = default; // support copying
Base& operator=(const Base&) = default;

};
  1. side effect of declaring a destructor
1
2
3
4
5
6
7
8
9
10
class StringTable {
public:
StringTable()
{ makeLogEntry("Creating StringTable object"); } // added
~StringTable() // also
{ makeLogEntry("Destroying StringTable object"); } // added
// other funcs as before
private:
std::map<int, std::string> values; // as before
};

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
2
3
4
5
6
7
8
class Widget {

template<typename T> // construct Widget
Widget(const T& rhs); // from anything
template<typename T> // assign Widget
Widget& operator=(const T& rhs); // from anything

};

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.
作者

cx

发布于

2022-07-20

更新于

2023-02-03

许可协议