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
    12
    std::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 后面的值匹配;

    表达式的值为 intcharshortenum 以及他们的 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
    33
    int 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
    8
    switch (init-statement; condition){
    // statements
    }
    // similar to
    init_statement;
    switch ( condition ){
    //statements;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct 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 后面的值须为常量表达式,可以为 intshortcharenum 以及他们的 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
    13
    void 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
    9
    switch (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
    12
    while (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-blockcondition 里面都处于作用域内;

    1
    2
    3
    4
    5
    6
    7
    8
    do {
    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 循环的运行顺序:

    1. init-statement 负责初始化一个值,这个值对于循环一直可见

    2. 然后判断 condition 是否为 true,如果为 true 则执行一次循环体;否则不会执行循环体,循环结束

    3. 执行 iteration-expression

    4. 2->3 一直循环,每次执行循环体之前一定会先验证 condition 是否依然成立

    1
    2
    3
    4
    5
    6
    7
    8
    9
    for (init-statement; condition; iter-expression){
    // loop body
    }
    // similar to
    init-statement;
    while (condition) {
    loop-body-statement;
    iter-expression;
    }
    • for 循环语句中定义的变量仅仅在 for 循环体内可见
    • for 循环头里面的 initializationexpression 都可以使用逗号运算符定于和更新多个变量;
    • for 循环头里面的三部分的任意一部分均可省略,之后在循环体内实现循环的更新和终止;
    • 空的 condition 相当于 true
  • range_for 的基本形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    for ( 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 is range.end()
    • Otherwise, begin-expr is std::begin(range) and end-expr is std::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 reference

    • TLDR

      • 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 automay lead to inadvertent changes of elements in the container. For example, consider the following example, which iterates over a vector of bools:

        1
        2
        3
        4
        std::vector<bool> v{false, false, false};
        for (auto x : v) {
        x = true; // Changes the element inside v!
        }

        After the loop ends, v will contain true, true, true, which is clearly something you would not expect. See this blog post for more details. Here, instead of using auto, it is better to explicitly specify the type (bool). With bool, 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 as std::unique_ptr. As auto 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 use const 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 & after auto. Therefore, I see no reason for using const auto. Use const 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 as auto& 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 of std::vector<bool> yields a temporary proxy object, which cannot bind to an lvalue reference (auto&).

      1
      2
      3
      std::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
      10
      struct 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:

      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 use auto&? As Howard Hinnant puts it, liberate use of auto&& results in so-called confuscated code: code that unnecessarily confuses people. My advice is to use auto& in non-generic code and auto&& 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 into for (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 over const auto&.

    • decltype(auto)

      1
      for (decltype(auto) x : range) // C++14

      C++14 introduced decltype(auto). It means: apply automatic type deduction, but use decltype rules. Whereas auto 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
    11
    try{
    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 支持空语句,比如使用循环的时候,在循环的条件部分就能达成自己的目的,则可以循环空语句;写空语句要加上注释表明故意为空;