Reading Note of CppPrimer-Chapter5
Statements
if 语句
cpp 规定悬垂 else(dangling else) 与离他最近的未匹配的 if 匹配,从而消除程序二义性;要使用花括号来控制代码块,从而确定执行逻辑
1
2
3
4
5
6
7
8// if 1
if(grade % 10 >= 3)
// if 2
if (grade % 10 > 7)
lettergrade += "+"
// match with if 2 rather than if 1
else:
letter_grade += "-";if 条件表达式里,条件表达式 (condition) 的前面可以写初始化表达式 (init-statement)
1
2
3
4
5
6
7
8
9
10
11
12std::map<int, std::string> m;
std::mutex mx;
extern bool shared_flag; // guarded by mx
if (auto it = m.find(10); it != m.end()) { return it->second.size(); }
if (char buf[10]; std::fgets(buf, 10, stdin)) { m[0] += buf; }
if (std::lock_guard lock(mx); shared_flag) { unsafe_ping(); shared_flag = false; }
if (int s; int count = ReadBytesWithSignal(&s)) { publish(count); raise(s); }
if (const auto keywords = {"if", "for", "while"};
std::ranges::any_of(keywords, [&tok](const char* kw) { return tok == kw; })) {
std::cerr << "Token must not be a keyword\n";
}
switch 语句
switch 控制结构 (condition)
switch
后面的括号里面是一个表达式,对求得的表达式的值与 case 后面的值匹配;表达式的值为
int
、char
、short
、enum
以及他们的unsigned
形式(可以做 integral promotion),也可以是可以隐式转换为以上类型的类的对象,或者是以上类型的初始化语句 (brace-or-equals initializer)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
33int get_intval(){
return 10;
}
//void switch_test(short a){ // valid, short
//void switch_test(int a=get_intval()){ // valid, brace-or-equals initializer
//void switch_test(char a){ // valid, char
void switch_test(float a){ // invalid, float
switch(a+1){ // error: statement requires expression of integer type ('float' invalid)
case 1:
std::cout << "1" << std::endl;
break;
default:
std::cout << "2" << std::endl;
break;
}
}
struct IntegralValue{
int a = 0;
operator int() const{ // implicitly convert the class to int
return a;
}
};
enum Color { red, green, blue };
//switch(red) { // valid, enum type
//switch (auto v = IntegralValue()) // valid, IntegralValue can be implicit conveted to int
switch(auto c = blue) { // valid, brace-or-equals initializer
case red : std::cout << "red\n"; brenoak;
case green: std::cout << "green\n"; break;
case blue : std::cout << "blue\n"; break;
}C++17 起,可以在 condition 前加上初始化表达式
1
2
3
4
5
6
7
8switch (init-statement; condition){
// statements
}
// similar to
init_statement;
switch ( condition ){
//statements;
}1
2
3
4
5
6
7
8
9struct Device {
enum { SLEEP, READY, BAD } state_{};
auto state() const { return state_; }
};
switch (auto dev = Device{}; dev.state()) {
case Device::SLEEP: /*...*/ break;
case Device::READY: /*...*/ break;
case Device::BAD: /*...*/ break;
}switch
case
default
语句case
后面的值须为常量表达式,可以为int
、short
、char
、enum
以及他们的unsigned
形式,不可为浮点数;cppreference 的说法是:a constant expression of the same type as the type of condition after conversions and integral promotions
所以比 interger 小的类型也是 OK 的,比如说
short, char, enum
等等;cpp17 里面可以对 switch 表达式的结果进行任意的转换(当没有可行的转换或者 integral promitions 的时候)以便匹配 case 的值;1
2
3
4
5
6
7
8
9
10
11
12
13void switch_test(int a){
int case1 = 1; // invalid
// constexpr short case1 = 1; // valid
// constexpr char case1 = 'a'; // valid
switch(a+1){
case case1: // error: case value is not a constexpr expression
std::cout << "1" << std::endl;
break;
default:
std::cout << "3" << std::endl;
break;
}
}当希望两个或者多个值共享一些操作,则可省略这些值之间的 break;
1
2
3
4
5
6
7
8
9switch (char ch=get_char()){
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
++vowelCount;
break;
}
while/do-while 语句
while
的条件表达式可以是一个赋值语句;如果是赋值表达式,则定义在while
条件部分和while
循环体内的变量每次迭代都要经历从创建到销毁的过程1
2
3
4
5
6
7
8
9
10
11
12while (T t=x){
statement;
}
// similar to
label:
{ // start of condition scope
T t = x;
if (t) {
statement
goto label; // calls the destructor of t
}
}无论循环体有几条语句,
while
循环都生成了一个循环体作用域 (loop body scope);函数体内定义的变量仅仅在函数体作用域可见do-while 和 while 的区别在于前者是先执行循环体后判断条件表达式,而后者是先判断条件表达式然后再执行循环体
do-while 的 condition 必须在循环体之前定义,使之可以在
do-block
和condition
里面都处于作用域内;1
2
3
4
5
6
7
8do {
v *= 1; // invalid, v is not defined
}while ((int v = get_value()) > 0);
int v = get_value();
do {
v *= 1; // valid, v is not defined
}while ((v = get_value()) > 0)
for/range-for 语句
理解 for 循环的运行顺序:
init-statement 负责初始化一个值,这个值对于循环一直可见
然后判断 condition 是否为 true,如果为 true 则执行一次循环体;否则不会执行循环体,循环结束
执行 iteration-expression
2->3 一直循环,每次执行循环体之前一定会先验证 condition 是否依然成立
1
2
3
4
5
6
7
8
9for (init-statement; condition; iter-expression){
// loop body
}
// similar to
init-statement;
while (condition) {
loop-body-statement;
iter-expression;
}- for 循环语句中定义的变量仅仅在 for 循环体内可见;
- for 循环头里面的 initialization 和 expression 都可以使用逗号运算符来定于和更新多个变量;
- for 循环头里面的三部分的任意一部分均可省略,之后在循环体内实现循环的更新和终止;
- 空的 condition 相当于 true
range_for 的基本形式
1
2
3
4
5
6
7
8
9for ( range-declaration : range-expression ){
// loop-statement
}
// similar to
auto && __range = range-expression;
for (auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin) {
range-declaration = *__begin;
loop-statement;
}- begin-expr is range and end-expr is (range + bound), where bound is the number of elements in the array
- if class type C has both a member named begin and a member named end then begin-expr is
range.begin()
and end-expr isrange.end()
- Otherwise, begin-expr is
std::begin(range)
and end-expr isstd::end(range)
- If range-expression returns a temporary, its lifetime is extended until the end of the loop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// the initializer may be a braced-init-list
for (int n : {0, 1, 2, 3, 4, 5})
std::cout << n << ' ';
// the initializer may be an array
int a[] = {0, 1, 2, 3, 4, 5};
for (int n : a)
std::cout << n << ' ';
// the initializer may be an container
std::vector<int> v = {0, 1, 2, 3, 4, 5};
for (const int& i : v) // access by const reference
std::cout << i << ' ';
for (auto i : v) // access by value, the type of i is int
std::cout << i << ' ';
for (auto& i : v) // access by non-const reference, the type of i is int&
std::cout << i << ' ';
const auto& cv = v;
for (auto&& i : cv) // access by forward reference, the type of i is const int&
std::cout << i << ' ';different type deduction for
auto
of range-for loop referenceTLDR
Use
auto
when you want to work with a copy of elements in the range.Use
auto&
when you want to modify elements in the range in non-generic code.Use
auto&&
when you want to modify elements in the range in generic code.Use
const auto&
when you want read-only access to elements in the range (even in generic code).Other variants are generally less useful.
auto
1
for (auto x : range)
This will create a copy of each element in the range. Therefore, use this variant when you want to work with a copy. For example, you may be iterating over a vector of strings and want to convert each string to uppercase and then pass it to a function. By using
auto
, a copy of each string will be provided for you. You can change it and pass forward.The following facts need to be kept in mind when using
auto
:Beware of containers returning proxy objects upon dereferencing of their iterators. Use of
auto
may lead to inadvertent changes of elements in the container. For example, consider the following example, which iterates over avector of bools
:1
2
3
4std::vector<bool> v{false, false, false};
for (auto x : v) {
x = true; // Changes the element inside v!
}After the loop ends,
v
will containtrue, true, true
, which is clearly something you would not expect. See this blog post for more details. Here, instead of usingauto
, it is better to explicitly specify the type (bool
). Withbool
, it will work as expected: the contents of the vector will be left unchanged.Using just
auto
will not work when iterating over ranges containing move-only types, such asstd::unique_ptr
. Asauto
creates a copy of each element in the range, the compilation will fail because move-only types cannot be copied.
const auto[useless]
1
for (const auto x : range)
The use of
const auto
may suggest that you want to work with an immutable copy of each element. However, when would you want this? Why not useconst auto&
? Why creating a copy when you will not be able to change it? And, even if you wanted this, from a code-review standpoint, it looks like you forgot to put&
afterauto
. Therefore, I see no reason for usingconst auto
. Useconst auto&
instead.auto&
1
for (auto& x : range)
Use
auto&
when you want to modify elements in the range in non-generic code. The first part of the previous sentence should be clear asauto&
will create references to the original elements in the range. To see why this code should not be used in generic code (e.g. inside templates), take a look at the following function template:1
2
3
4
5
6
7// Sets all elements in the given range to the given value.
template<typename Range, typename Value>
void set_all_to(Range& range, const Value& value) {
for (auto& x : range) {
x = value;
}
}It will work. Well, most of the time. Until someone tries to use it on the dreaded
std::vector<bool>
. Then, the example will fail to compile because dereferencing an iterator ofstd::vector<bool>
yields a temporary proxy object, which cannot bind to an lvalue reference (auto&
).1
2
3std::vector<bool> v(10);
for (auto& e : v)
e = true;1
error: non-const lvalue reference to type '__bit_reference<...>' cannot bind to a temporary of type '__bit_reference<...>'
As we will see shortly, the solution is to use one more
&
when writing generic code.const auto&
1
for (const auto& x : range)
Use
const auto&
when you want read-only access to elements in the range, even in generic code. This is the number one choice for iterating over a range when all you want to is read its elements. No copies are made and the compiler can verify that you indeed do not modify the elements.Nevertheless, keep in mind that even though you will not be able to modify the elements in the range directly, you may still be able to modify them indirectly. For example, when the elements in the range are smart pointers:
1
2
3
4
5
6
7
8
9
10struct Person {
std::string name;
// ...
};
std::vector<std::unique_ptr<Person>> v;
// ...
for (const auto& x : v) {
x->name = "John Doe"; // This will change the name of all people in v.
}In such situations, you have to pay close attention to what you are doing because the compiler will not help you, even if you write
const auto&
.auto&&
1
for (auto&& x : range)
Use
auto&&
when you want to modify elements in the range in generic code. To elaborate,auto&&
is a forwarding reference, also known as a universal reference. It behaves as follows:- When initialized with an lvalue, it creates an lvalue reference.
- When initialized with an rvalue, it creates an rvalue reference.
A detailed explanation of forwarding references is outside of scope of the present post. For more details, see this article by Scott Meyers. Anyway, the use of
auto&&
allows us to write generic loops that can also modify elements of ranges yielding proxy objects, such as our friend (or foe?)std::vector<bool>
:1
2
3
4
5
6
7
8// Sets all elements in the given range to the given value.
// Now working even with std::vector<bool>.
template<typename Range, typename Value>
void set_all_to(Range& range, const Value& value) {
for (auto&& x : range) { // Notice && instead of &.
x = value;
}
}Now, you may wonder: if
auto&&
works even in generic code, why should I ever useauto&
? As Howard Hinnant puts it, liberate use ofauto&&
results in so-called confuscated code: code that unnecessarily confuses people. My advice is to useauto&
in non-generic code andauto&&
only in generic code.By the way, there was a proposal for C++1z to allow writing just
for (x : range)
, which would be translated intofor (auto&& x : range)
. Such range-based for loops were called terse. However, this proposal was removed from consideration and will not be part of C++.const auto&&[useless]
1
for (const auto&& x : range)
This variant will bind only to rvalues, which you will not be able to modify or move because of the
const
. This makes it less than useless. Hence, there is no reason for choosing this variant overconst auto&
.decltype(auto)
1
for (decltype(auto) x : range) // C++14
C++14 introduced
decltype(auto)
. It means: apply automatic type deduction, but usedecltype
rules. Whereasauto
strips down top-level cv qualifiers and references,decltype
preserves them.As is stated in this C++ FAQ,
decltype(auto)
is primarily useful for deducing the return type of forwarding functions and similar wrappers. However, it is not intended to be a widely used feature beyond that. And indeed, there seems to be no reason for using it in range-based for loops.
try catch
try catch 有三种形式
1
2
3
4
5
6
7
8// 1) Catch-clause that declares a named formal parameter
try { /* */ } catch (const std::exception& e) { /* */ }
// 2) Catch-clause that declares an unnamed parameter
try { /* */ } catch (const std::exception&) { /* */ }
// 3) Catch-all handler, which is activated for any exception
try { /* */ } catch (...) { /* */ }example code
1
2
3
4
5
6
7
8
9
10
11try{
f();
}
catch (const std::overflow_error& e)
{} // this executes if f() throws std::overflow_error (same type rule)
catch (const std::runtime_error& e)
{} // this executes if f() throws std::underflow_error (base class rule)
catch (const std::exception& e)
{} // this executes if f() throws std::logic_error (base class rule)
catch (...)
{} // this executes if f() throws std::string or int or any other unrelated type如果最后还是没有匹配的 exception handler,会调用
std::terminate
break/continue/goto
break 仅仅作用于最近的循环或者 switch,不要期望一个 break 可以跳出多重循环;
continue 只能用于 for、while、do while 中,包含 range for;
goto 语句的标签标示符独立于变量和其他标示符的名字,因为 goto 标签标示符可以和程序中的其他实体的标示符使用同一个名字而不会互相干扰;
try block 里面定义的变量在 catch 里面是无法访问的,类似于 do while 里面 while 定义的变量在 loop body 里面不能访问、switch 语句后面 case 里面定义的变量前面 case 不能访问一样
try catch 寻找 exception handler 的顺序和函数调用顺序相反,出现 exception 的时候往调用函数的反方向找,一直找到可以处理的 catch 语句为止,没有找到的时候会程序调用 terminate,非正常退出;
misc
- cpp 支持空语句,比如使用循环的时候,在循环的条件部分就能达成自己的目的,则可以循环空语句;写空语句要加上注释表明故意为空;