diff --git a/book/en-us/02-usability.md b/book/en-us/02-usability.md index 6da7d8e5..eb8a21aa 100644 --- a/book/en-us/02-usability.md +++ b/book/en-us/02-usability.md @@ -18,20 +18,21 @@ which refers to the language behavior that occurred before the runtime. ### nullptr -The purpose of `nullptr` appears to replace `NULL`. In a sense, -traditional C++ treats `NULL` and `0` as the same thing, -depending on how the compiler defines NULL, -and some compilers define NULL as `((void*)0)` Some will define it directly as `0`. +The purpose of `nullptr` appears to replace `NULL`. There are **null pointer constants** in the C and C++ languages, +which can be implicitly converted to null pointer value of any pointer type, +or null member pointer value of any pointer-to-member type in C++. +`NULL` is provided by the standard library implementation and defined as an implementation-defined null pointer constant. +In C, some standard libraries defines `NULL` as `((void*)0)` and some define it as `0`. -C++ **does not allow** to implicitly convert `void *` to other types. -But if the compiler tries to define `NULL` as `((void*)0)`, then in the following code: +C++ **does not allow** to implicitly convert `void *` to other types, and thus `((void*)0)` is not a valid implementation +of `NULL`. If the standard library tries to define `NULL` as `((void*)0)`, then compilation error would occur in the following code: ```cpp char *ch = NULL; ``` C++ without the `void *` implicit conversion has to define `NULL` as `0`. -This still creates a new problem. Defining `NULL` to 0 will cause the overloading feature in `C++` to be confusing. +This still creates a new problem. Defining `NULL` to `0` will cause the overloading feature in `C++` to be confusing. Consider the following two `foo` functions: ```cpp @@ -41,7 +42,7 @@ void foo(int); Then the `foo(NULL);` statement will call `foo(int)`, which will cause the code to be counterintuitive. -To solve this problem, C++11 introduced the `nullptr` keyword, which is specifically used to distinguish null pointers, 0. The type of `nullptr` is `nullptr_t`, which can be implicitly converted to any pointer or member pointer type, and can be compared equally or unequally with them. +To solve this problem, C++11 introduced the `nullptr` keyword, which is specifically used to distinguish null pointers, `0`. The type of `nullptr` is `nullptr_t`, which can be implicitly converted to any pointer or member pointer type, and can be compared equally or unequally with them. You can try to compile the following code using clang++: @@ -417,17 +418,23 @@ auto i = 5; // i as int auto arr = new auto(10); // arr as int * ``` -Since C++ 20, `auto` can even be used as function arguments. Consider -the following example: +Since C++ 14, `auto` can even be used as function arguments in generic lambda expressions, +and such functionality is generalized to normal functions in C++ 20. +Consider the following example: ```cpp -int add(auto x, auto y) { +auto add14 = [](auto x, auto y) -> int { + return x+y; +} + +int add20(auto x, auto y) { return x+y; } auto i = 5; // type int auto j = 6; // type int -std::cout << add(i, j) << std::endl; +std::cout << add14(i, j) << std::endl; +std::cout << add20(i, j) << std::endl; ``` > **Note**: `auto` cannot be used to derive array types yet: @@ -480,7 +487,7 @@ type z == type x ### tail type inference -You may think that when we introduce `auto`, we have already mentioned that `auto` cannot be used for function arguments for type derivation. Can `auto` be used to derive the return type of a function? Still consider an example of an add function, which we have to write in traditional C++: +You may think that whether `auto` can be used to deduce the return type of a function. Still consider an example of an add function, which we have to write in traditional C++: ```cpp template diff --git a/book/en-us/03-runtime.md b/book/en-us/03-runtime.md index a61d6654..8ecd2956 100644 --- a/book/en-us/03-runtime.md +++ b/book/en-us/03-runtime.md @@ -87,8 +87,8 @@ capture lists can be: - \[\] empty capture list - \[name1, name2, ...\] captures a series of variables -- \[&\] reference capture, let the compiler deduce the reference list by itself -- \[=\] value capture, let the compiler deduce the value list by itself +- \[&\] reference capture, determine the reference capture list from the uses the in function body +- \[=\] value capture, determine the value capture list from the uses in the function body #### 4. Expression capture diff --git a/book/en-us/07-thread.md b/book/en-us/07-thread.md index 379d4d2a..a9f41162 100644 --- a/book/en-us/07-thread.md +++ b/book/en-us/07-thread.md @@ -31,11 +31,11 @@ int main() { We have already learned the basics of concurrency technology in the operating system, or the database, and `mutex` is one of the cores. C++11 introduces a class related to `mutex`, with all related functions in the `` header file. -`std::mutex` is the most basic `mutex` class in C++11, and you can create a mutex by instantiating `std::mutex`. +`std::mutex` is the most basic mutex class in C++11, and a mutex can be created by constructing a `std::mutex` object. It can be locked by its member function `lock()`, and `unlock()` can be unlocked. But in the process of actually writing the code, it is best not to directly call the member function, Because calling member functions, you need to call `unlock()` at the exit of each critical section, and of course, exceptions. -At this time, C++11 also provides a template class `std::lock_guard` for the RAII syntax for the mutex. +At this time, C++11 also provides a template class `std::lock_guard` for the RAII mechanism for the mutex. RAII guarantees the exceptional security of the code while keeping the simplicity of the code. @@ -67,7 +67,9 @@ int main() { ``` Because C++ guarantees that all stack objects will be destroyed at the end of the declaration period, such code is also extremely safe. -Whether `critical_section()` returns normally or if an exception is thrown in the middle, a stack rollback is thrown, and `unlock()` is automatically called. +Whether `critical_section()` returns normally or if an exception is thrown in the middle, a stack unwinding is thrown, and `unlock()` is automatically called. + +> An exception is thrown and not caught (it is implementation-defined whether any stack unwinding is done in this case). `std::unique_lock` is more flexible than `std::lock_guard`. Objects of `std::unique_lock` manage the locking and unlocking operations on the `mutex` object with exclusive ownership (no other `unique_lock` objects owning the ownership of a `mutex` object). So in concurrent programming, it is recommended to use `std::unique_lock`. @@ -158,7 +160,7 @@ After encapsulating the target to be called, you can use `get_future()` to get a The condition variable `std::condition_variable` was born to solve the deadlock and was introduced when the mutex operation was not enough. For example, a thread may need to wait for a condition to be true to continue execution. A dead wait loop can cause all other threads to fail to enter the critical section so that when the condition is true, a deadlock occurs. -Therefore, the `condition_variable` instance is created primarily to wake up the waiting thread and avoid deadlocks. +Therefore, the `condition_variable` object is created primarily to wake up the waiting thread and avoid deadlocks. `notify_one()` of `std::condition_variable` is used to wake up a thread; `notify_all()` is to notify all threads. Below is an example of a producer and consumer model: @@ -228,7 +230,7 @@ We simply can't expect multiple consumers to be able to produce content in a par ## 7.5 Atomic Operation and Memory Model Careful readers may be tempted by the fact that the example of the producer-consumer model in the previous section may have compiler optimizations that cause program errors. -For example, the boolean `notified` is not modified by `volatile`, and the compiler may have optimizations for this variable, such as the value of a register. +For example, the compiler may have optimizations for the variable `notified`, such as the value of a register. As a result, the consumer thread can never observe the change of this value. This is a good question. To explain this problem, we need to further discuss the concept of the memory model introduced from C++11. Let's first look at a question. What is the output of the following code? ```cpp @@ -274,8 +276,8 @@ This is a very strong set of synchronization conditions, in other words when it This seems too harsh for a variable that requires only atomic operations (no intermediate state). The research on synchronization conditions has a very long history, and we will not go into details here. Readers should understand that under the modern CPU architecture, atomic operations at the CPU instruction level are provided. -Therefore, in the C++11 multi-threaded shared variable reading and writing, the introduction of the `std::atomic` template, so that we instantiate an atomic type, will be an -Atomic type read and write operations are minimized from a set of instructions to a single CPU instruction. E.g: +Therefore, the `std::atomic` template is introduced in C++11 for the topic of multi-threaded shared variable reading and writing, which enables us to instantiate atomic types, +and minimize an atomic read or write operation from a set of instructions to a single CPU instruction. E.g: ```cpp std::atomic counter; diff --git a/book/zh-cn/02-usability.md b/book/zh-cn/02-usability.md index ed4fe982..e9c84e9a 100644 --- a/book/zh-cn/02-usability.md +++ b/book/zh-cn/02-usability.md @@ -14,9 +14,9 @@ order: 2 ### nullptr -`nullptr` 出现的目的是为了替代 `NULL`。在某种意义上来说,传统 C++ 会把 `NULL`、`0` 视为同一种东西,这取决于编译器如何定义 `NULL`,有些编译器会将 `NULL` 定义为 `((void*)0)`,有些则会直接将其定义为 `0`。 +`nullptr` 出现的目的是为了替代 `NULL`。 C 与 C++ 语言中有**空指针常量**,它们能被隐式转换成任何指针类型的空指针值,或 C++ 中的任何成员指针类型的空成员指针值。 `NULL` 由标准库实现提供,并被定义为实现定义的空指针常量。在 C 中,有些标准库会把 `NULL` 定义为 `((void*)0)` 而有些将它定义为 `0`。 -C++ **不允许**直接将 `void *` 隐式转换到其他类型。但如果编译器尝试把 `NULL` 定义为 `((void*)0)`,那么在下面这句代码中: +C++ **不允许**直接将 `void *` 隐式转换到其他类型,从而 `((void*)0)` 不是 `NULL` 的合法实现。如果标准库尝试把 `NULL` 定义为 `((void*)0)`,那么下面这句代码中会出现编译错误: ```cpp char *ch = NULL; @@ -354,17 +354,22 @@ auto i = 5; // i 被推导为 int auto arr = new auto(10); // arr 被推导为 int * ``` -从 C++ 20 起,`auto` 甚至能用于函数传参,考虑下面的例子: +从 C++ 14 起,`auto` 能用于 lambda 表达式中的函数传参,而 C++ 20 起该功能推广到了一般的函数。考虑下面的例子: ```cpp -int add(auto x, auto y) { +auto add14 = [](auto x, auto y) -> int { return x+y; } -auto i = 5; // 被推导为 int -auto j = 6; // 被推导为 int -std::cout << add(i, j) << std::endl; +int add20(auto x, auto y) { + return x+y; +} + +auto i = 5; // type int +auto j = 6; // type int +std::cout << add14(i, j) << std::endl; +std::cout << add20(i, j) << std::endl; ``` > @@ -413,7 +418,7 @@ type z == type x ### 尾返回类型推导 -你可能会思考,在介绍 `auto` 时,我们已经提过 `auto` 不能用于函数形参进行类型推导,那么 `auto` 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写: +你可能会思考, `auto` 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写: ```cpp template @@ -958,7 +963,7 @@ enum class new_enum : unsigned int { ```cpp if (new_enum::value3 == new_enum::value4) { - // 会输出 + // 会输出true std::cout << "new_enum::value3 == new_enum::value4" << std::endl; } ``` diff --git a/book/zh-cn/03-runtime.md b/book/zh-cn/03-runtime.md index 1760b3a8..94517dc7 100644 --- a/book/zh-cn/03-runtime.md +++ b/book/zh-cn/03-runtime.md @@ -76,8 +76,8 @@ void lambda_reference_capture() { - \[\] 空捕获列表 - \[name1, name2, ...\] 捕获一系列变量 -- \[&\] 引用捕获, 让编译器自行推导引用列表 -- \[=\] 值捕获, 让编译器自行推导值捕获列表 +- \[&\] 引用捕获, 从函数体内的使用确定引用捕获列表 +- \[=\] 值捕获, 从函数体内的使用确定值捕获列表 #### 4. 表达式捕获 diff --git a/book/zh-cn/07-thread.md b/book/zh-cn/07-thread.md index 8e11407d..3c59a72d 100644 --- a/book/zh-cn/07-thread.md +++ b/book/zh-cn/07-thread.md @@ -31,11 +31,12 @@ int main() { 我们在操作系统、亦或是数据库的相关知识中已经了解过了有关并发技术的基本知识,`mutex` 就是其中的核心之一。 C++11 引入了 `mutex` 相关的类,其所有相关的函数都放在 `` 头文件中。 -`std::mutex` 是 C++11 中最基本的 `mutex` 类,通过实例化 `std::mutex` 可以创建互斥量, +`std::mutex` 是 C++11 中最基本的互斥量类,可以通过构造 `std::mutex` 对象创建互斥量, 而通过其成员函数 `lock()` 可以进行上锁,`unlock()` 可以进行解锁。 但是在实际编写代码的过程中,最好不去直接调用成员函数, 因为调用成员函数就需要在每个临界区的出口处调用 `unlock()`,当然,还包括异常。 -这时候 C++11 还为互斥量提供了一个 RAII 语法的模板类 `std::lock_guard`。 +而 C++11 为互斥量提供了一个 RAII 机制的模板类 `std::lock_guard`。 + RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。 在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分,例如: @@ -68,7 +69,9 @@ int main() { ``` 由于 C++ 保证了所有栈对象在生命周期结束时会被销毁,所以这样的代码也是异常安全的。 -无论 `critical_section()` 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 `unlock()`。 +无论 `critical_section()` 正常返回、还是在中途抛出异常,都会引发栈回溯,也就自动调用了 `unlock()`。 + +> 没有捕获抛出的异常(此时由实现定义是否进行栈回溯)。 而 `std::unique_lock` 则是相对于 `std::lock_guard` 出现的,`std::unique_lock` 更加灵活, `std::unique_lock` 的对象会以独占所有权(没有其他的 `unique_lock` 对象同时拥有某个 `mutex` 对象的所有权) @@ -160,7 +163,7 @@ int main() { 条件变量 `std::condition_variable` 是为了解决死锁而生,当互斥操作不够用而引入的。 比如,线程可能需要等待某个条件为真才能继续执行, 而一个忙等待循环中可能会导致所有其他线程都无法进入临界区使得条件为真时,就会发生死锁。 -所以,`condition_variable` 实例被创建出现主要就是用于唤醒等待线程从而避免死锁。 +所以,`condition_variable` 对象被创建出现主要就是用于唤醒等待线程从而避免死锁。 `std::condition_variable`的 `notify_one()` 用于唤醒一个线程; `notify_all()` 则是通知所有线程。下面是一个生产者和消费者模型的例子: @@ -232,7 +235,7 @@ int main() { ## 7.5 原子操作与内存模型 细心的读者可能会对前一小节中生产者消费者模型的例子可能存在编译器优化导致程序出错的情况产生疑惑。 -例如,布尔值 `notified` 没有被 `volatile` 修饰,编译器可能对此变量存在优化,例如将其作为一个寄存器的值, +例如,编译器可能对变量 `notified` 存在优化,例如将其作为一个寄存器的值, 从而导致消费者线程永远无法观察到此值的变化。这是一个好问题,为了解释清楚这个问题,我们需要进一步讨论 从 C++ 11 起引入的内存模型这一概念。我们首先来看一个问题,下面这段代码输出结果是多少? @@ -242,7 +245,7 @@ int main() { int main() { int a = 0; - int flag = 0; + volatile int flag = 0; std::thread t1([&]() { while (flag != 1); @@ -280,8 +283,7 @@ int main() { 这对于一个仅需原子级操作(没有中间态)的变量,似乎太苛刻了。 关于同步条件的研究有着非常久远的历史,我们在这里不进行赘述。读者应该明白,现代 CPU 体系结构提供了 CPU 指令级的原子操作, -因此在 C++11 中多线程下共享变量的读写这一问题上,还引入了 `std::atomic` 模板,使得我们实例化一个原子类型,将一个 -原子类型读写操作从一组指令,最小化到单个 CPU 指令。例如: +因此在多线程下共享变量的读写这一问题上, C++11 中还引入了 `std::atomic` 模板,使得我们能实例化原子类型,并将一个原子写操作从一组指令,最小化到单个 CPU 指令。例如: ```cpp std::atomic counter;