cpp20入门读书笔记
目前编译器对 c++20 只部分支持, 请谨慎使用
1. Basic Ideas
2. Introducing Fundamental Types of Data
int braced_init {15}; |
Type signed char is always one byte
The signed modifier is mostly optional; if omitted, your type will be signed by default. The only exception to this rule is char. While the unmodified type char does exist, it is compiler-dependent whether it is signed or unsigned.
Only use variables of the unmodified char type to store letter characters. To store numbers, you should use either signed char or unsigned char. std::byte type to store binary data
int counter {}; // counter starts at zero |
Ever since C++14, you can use the single quote character, ', to make numeric literals more readable. Here’s an example: 22'333 -1'234LL 12'345ULL
there are situations where you do need to add the correct literal suffixes, such as when you initialize a variable with type auto
Binary literals were introduced by the C++14 standard. You write a binary integer literal as a sequence of binary digits (0 or 1) prefixed by either 0b or 0B, eg: 0b1010101
import <numbers>;
std::numbers::e
std::numbers::e_v<float>
特定长度 #include <cmath>
implicit conversions . The way this works is that the variable of a type with the more limited range is converted to the type of the other. order:signed 优先度低于 unsigned
long double |
std::format
available in cpp20
std::cout << std::format("Pond diameter required for {} fish is {:.2} feet.\n", fish_count, pond_diameter); |
<limits>
std::cout << "Maximum value of type double is " << std::numeric_limits<double>::max(); |
Type wchar_t is a fundamental type intended for character sets where a single character does not fit into one byte. Hence its name: wchar_t derives from wide character , because the character is “wider” than the usual one-byte character. By contrast, type char is referred to as “narrow” because of the limited range of character codes that are available. You define wide-character literals in a similar way to literals of type char, but you prefix them with L. Here’s an example:wchar_t z {L'Z'};
use char8_t, char16_t, or char32_t instead. Values of these types are intended to store characters encoded as UTF-8, UTF-16, or UTF-32, their prefix are: u8, u16/u, U
auto
Caution You need to be careful when using braced initializers with the auto keyword. For example, suppose you write this (notice the equals sign!):
auto m = {10}; |
To summarize, if your compiler properly supports C++17, you can use braced initialization to initialize any variable with a single value, provided you do not combine it with an assignment. If your compiler is not fully up-to-date yet, however, you should simply never use braced initializers with auto. Instead, either explicitly state the type or use assignment or functional notation.
3. Working with Fundamental Data Types
Remember that the lifetime and scope of a variable are different things. Lifetime is the period of execution time over which a variable survives. Scope is the region of program code over which the variable name can be used. It’s important not to get these two ideas confused.
Variables defined outside of all blocks and classes are also called globals and have global scope (which is also called global namespace scope). This means they’re accessible in all the functions in the source file following the point at which they’re defined. If you define them at the beginning of a source file, they’ll be accessible throughout the file. In Chapter 11, we’ll show how to declare variables that can be used in multiple files.Global variables have static storage duration by default. use ::
to access
The using keyword has many uses:
It allows you to refer to (specific or all) enumerators of scoped enumerations without specifying the enumeration’s name as scope.
It allows you to refer to (specific or all) types and functions of a namespace without specifying the namespace’s name as scope.
It allows you to define aliases for other types. In legacy code, you might still encounter typedef being used for the same purpose.
4. Making Decisions
5. Arrays and Loops
for range loop
int main()
{
std::vector<int> v = {0, 1, 2, 3, 4, 5};
for (const int& i : v) // access by const reference
std::cout << i << ' ';
std::cout << '\n';
for (auto i : v) // access by value, the type of i is int
std::cout << i << ' ';
std::cout << '\n';
for (auto&& i : v) // access by forwarding reference, the type of i is int&
std::cout << i << ' ';
std::cout << '\n';
const auto& cv = v;
for (auto&& i : cv) // access by f-d reference, the type of i is const int&
std::cout << i << ' ';
std::cout << '\n';
for (int n : {0, 1, 2, 3, 4, 5}) // the initializer may be a braced-init-list
std::cout << n << ' ';
std::cout << '\n';
int a[] = {0, 1, 2, 3, 4, 5};
for (int n : a) // the initializer may be an array
std::cout << n << ' ';
std::cout << '\n';
for ([[maybe_unused]] int n : a)
std::cout << 1 << ' '; // the loop variable need not be used
std::cout << '\n';
for (auto n = v.size(); auto i : v) // the init-statement (C++20)
std::cout << --n + i << ' ';
std::cout << '\n';
for (typedef decltype(v)::value_type elem_t; elem_t i : v)
// typedef declaration as init-statement (C++20)
std::cout << i << ' ';
std::cout << '\n';
for (using elem_t = decltype(v)::value_type; elem_t i : v)
// alias declaration as init-statement (C++23)
std::cout << i << ' ';
std::cout << '\n';
}
6. Pointers and References
Low-level dynamic memory manipulation is synonymous for a wide range of serious hazards such as dangling pointers, multiple deallocations, deallocation mismatches, memory leaks, and so on. Our golden rule is therefore this: never use the low-level new/new[] and delete/delete[] operators directly. Containers (and std::vector<> in particular) and smart pointers are nearly always the better choice!
float const * const pvalue {&value};
read from right to left, * means "pointer to"
new delete
It is perfectly safe to apply delete on a pointer variable that holds the value nullptr. The statement then has no effect at all. Using if tests such as the following is therefore not necessary
Note that the delete operator frees the memory but does not change the pointer. After the previous statement has executed, pvalue still contains the address of the memory that was allocated, but the memory is now free and may be allocated immediately to something else. A pointer that contains such a spurious address is sometimes called a dangling pointer. always resetting a pointer when you release the memory to which it points, like this:
delete pvalue; // Release memory pointed to by pvalue |
如果二次 delete 会报错捏
smart pointer
Smart pointers are normally used only to store the address of memory allocated in the free store.
By far the most notable feature of a smart pointer is that you don’t have to worry about using the delete or delete[] operator to free the memory. It will be released automatically when it is no longer needed. This means that multiple deallocations, allocation/deallocation mismatches, and memory leaks will no longer be possible. If you consistently use smart pointers, dangling pointers will be a thing of the past as well.
Smart pointer types are defined by templates inside the<memory>
module of the Standard Library
unique_ptr<T>
auto pdata { std::make_unique<double>(999.0) };
cout << pdata << endl;//指针地址
cout << *pdata << endl;//指向的值 999.0
auto pdata{std::make_unique<Foo>(Foo{1})};
cout << pdata << endl; // 指针地址
cout << pdata.get() << endl;// 指针地址, 注意是.get()
pdata.reset(new Foo{2});
Foo* p_foo {pdata.release()};
auto pvalues{ std::make_unique<double[]>(n) };In other words, there can never be two or more unique_ptr<T> objects pointing to the same memory address at the same time. A unique_ptr<> object is said to own what it points to exclusively.
shared_ptr<T>
auto pdata{ std::make_shared<double>(999.0) }
std::shared_ptr<double> pdata2 {pdata};there can be any number of shared_ptr
objects that contain—or, share—the same address. The reference count for a shared_ptr<> containing a given free store address is incremented each time a new shared_ptr<> object is created containing that address, and it’s decremented when a shared_ptr<> containing the address is destroyed or assigned to point to a different address. All shared_ptr<> objects that point to the same address have access to the count of how many there are. weak_ptr<T>
A weak_ptr<T> is linked to a shared_ptr<T> and contains the same address. Creating a weak_ptr<> does not increment the reference count associated with the linked shared_ptr<> object, though, so a weak_ptr<> does not prevent the object pointed to from being destroyed.
One use for having weak_ptr<> objects is to avoid so- called reference cycles with shared_ptr<> objects. Conceptually, a reference cycle is where a shared_ptr<> inside an object x points to some other object y that contains a shared_ptr<>, which points back to x.
reference in loop
double sum {}; |
7. Working with Strings
- Internally, the terminating null character is still present in the array managed by a std::string object, but only for compatibility with legacy and/or C functions. As a user of std::string, you normally do not need to know that it even exists. All string functionality transparently deals with this legacy character for you.
- You can use the + operator to concatenate a string object with a string literal, a character, or another string object.
std::to_string() std::stoi() std::stod()
8. Defining Functions
- Input parameters should be reference-to-const, except for smaller values such as those of fundamental types. Returning values is generally preferred over output parameters.
- Returning a reference from a function allows the function to be used on the left of an assignment operator. Specifying the return type as a reference-to-const prevents this.
9. Vocabulary Types
Use std::optional<> to represent any value that may or may not be present. Examples are optional inputs to a function or the result of a function that may fail. This makes your code self-documenting and therefore safer. As of C++17, the Standard Library provides std::optional <>, designed to replace all implicit encodings of optional values that we showed earlier.
std::optional<int>
find_last(const std::string& s,
char c,
std::optional<int> startIndex);
std::optional<int>
read_int_setting(const std::string& file,
const std::string& setting);We replaced it with a default value that is equal to
std::nullopt
. This special constant is defined by the Standard Library to initialize optional<T> values that do not (yet) have a T value assigned. To check:
you have the compiler convert the optional<> to a Boolean for you, you call the has_value() function, or you compare the optional<> to nullopt . To get: you can either use the *** operator** or call the value() function. Assigning the optionalreturn value directly to a size_t, however, would not be possible. The compiler cannot convert values of type optional to values of type size_t. Use std::string_view instead of const std::string& to avoid inadvertent copies of string literals or other character arrays.
void find_words(std::vector<std::string>& words,
const std::string& text,
const std::string& separators);
int main() {
std::string text; // The string to be searched
std::cout << "Enter some text terminated by *:\n";
std::getline(std::cin, text, '*');
const std::string separators{" ,;:.\"!?'\n"};
std::vector<std::string> words; // Words found
find_words(
words, text, " ,;:.\"!?'\n"); /* no more 'separators' constant! */
//list_words(words);
}
// 这里, find_words 第三个参数传入的是字符串常量(const char[])而不是引用
//编译器会隐式临时复制, 然后传入这个复制的引用The compiler, however, will refuse any and all implicit conversions of std::string_view objects to values of type std::string (give it a try!).
string_view does not provide a c_str() function to convert it to a const char* array.
std::string{my_view}.
Use std::span<const T> instead of, for instance, const std::vector<T>& parameters to make the same function work as well for C-style arrays, std::array<> objects, etc.
Similarly, use std::span<T> instead of std::vector<T>& parameters, unless you need the ability to add or remove elements.
The reason is that there is one significant difference between a span and a view: a span <>, unlike a
string_view , allows you to reassign or change the elements of the underlying array. While a span<> allows you to reassign or otherwise alter elements, it does not allow you to add or remove any elements. That is, a span<> does not offer members such as push_back(), erase(), or clear(). Otherwise, a span<> could never be created for C-style arrays or std::array<> objects.
Use std::span<(const) T,N> instead of (const) std::array<T,N>& parameters to make the same function work for C-style arrays (or other containers you know to contain at least N elements).
10. Function Templates
need examples
11. Modules and Namespaces
C++ is object oriented, in the sense that it supports the object oriented paradigm for software development.
However, differently from Java, C++ doesn't force you to group function definitions in classes: the standard C++ way for declaring a function is to just declare a function, without any class.
If instead you are talking about method declaration/definition then the standard way is to put just the declaration in an include file (normally named .h
or .hpp
) and the definition in a separate implementation file (normally named .cpp
or .cxx
). I agree this is indeed somewhat annoying and requires some duplication but it's how the language was designed (the main concept is that C++ compilation is done one unit at a time: you need the .cpp of the unit being compiled and just the .h of all the units being used by the compiled code; in other words the include file for a class must contain all the information needed to be able to generate code that uses the class). There are a LOT of details about this, with different implications about compile speed, execution speed, binary size and binary compatibility.
For quick experiments anything works... but for bigger projects the separation is something that is practically required (even if it may make sense to keep some implementation details in the public .h).
Note: Even if you know Java, C++ is a completely different language... and it's a language that cannot be learned by experimenting. The reason is that it's a rather complex language with a lot of asymmetries and apparently illogical choices, and most importantly, when you make a mistake there are no "runtime error angels" to save you like in Java... but there are instead "undefined behavior daemons".
The only reasonable way to learn C++ is by reading... no matter how smart you are there is no way you can guess what the committee decided (actually being smart is sometimes even a problem because the correct answer is illogical and a consequence of historical heritage.)
Just pick a good book or two and read them cover to cover.
12. Defining Your Own Data Types
- const member functions can’t modify the member variables of a class object unless the member variables have been declared as mutable.
13. Operator Overloading
For a unary operator defined as a class member function, the operand is the class object. For a unary operator defined as a global operator function, the operand is the function parameter.
For a binary operator function declared as a member of a class, the left operand is the class object, and the right operand is the function parameter. For a binary operator defined by a global operator function, the first parameter specifies the left operand, and the second parameter specifies the right operand.
If you overload operators == and <=>, you get operators !=, <, >, <=, and >= all for free. In many cases you can even have the compiler generate the code for you.
14. Inheritance
A derived class constructor can, and often should, explicitly call constructors for its direct bases in the initialization list for the constructor. If you don’t call one explicitly, the base class’s default constructor is called. A copy constructor in a derived class, for one, should always call the copy constructor of all direct base classes.
15. Polymorphism
You should use the override qualifier with each member function of a derived class that overrides a virtual base class member. This causes the compiler to verify that the functions signatures in the base and derived classes are, and forever remain, the same.
class Carton : public Box |
The dynamic_cast<> operator is generally used to cast from a pointer-to-a-polymorphic-base-class to a pointer- to-a-derived-class. If the pointer does not point to an object of the given derived class type, dynamic_cast<> evaluates to nullptr. This type check is performed dynamically, at runtime.
16. Runtime Errors and Exceptions
try |
If an exception isn’t caught by any catch block, then the std::terminate() function is called, which immediately aborts the program execution.
The noexcept specification for a function indicates that the function does not throw exceptions. If a noexcept function does throw an exception it does not catch, std::terminate() is called.
Even if a destructor does not have an explicit noexcept specifier, the compiler will almost always generate one for you. This implies that you must never allow an exception to leave a destructor; otherwise, std::terminate() will be triggered.
The Standard Library defines a range of standard exception types in the
17. Class Templates
Always use the copy-and-swap idiom to implement the copy assignment operator in terms of the copy constructor and a (noexcept) swap() function. Use the const-and-back-again idiom to implement non- const overloads in terms of const overloads of the same member function to avoid having to repeat yourself. This is an example of the DRY principle (Don’t Repeat Yourself), which advocates avoiding code duplication at all costs.
18. Move Semantics
std::move() can be used to convert an lvalue (such as a named variable) into an rvalue. Take care, though. Once moved, an object should normally not be used anymore.
std::move() Does Not Move
Make no mistake, std::move() does not move anything. All this function does is turn a given lvalue into an rvalue reference.
If there’s no move assignment operator for Array<> to accept the rvalue, the copy assignment operator will be used instead. So, always remember, adding std::move() is of no consequence if the function or constructor that you are passing a value to has no overload with an rvalue reference parameter!
Array<std::string> more_strings{ 2'000 }; Array<std::string>&& rvalue_ref{ std::move(more_strings) }; |
Notwithstanding that the rvalue_ref variable clearly has an rvalue reference type, the output of the program will show that the corresponding object is copied: Array of 1000 elements moved (assignment) Array of 2000 elements copied
Every variable name expression is an lvalue, even if the type of that variable is an rvalue reference type. To move the contents of a named variable, you must therefore always add std::move():
strings = std::move(rvalue_ref); |
One way to work around this duplication is to redefine the const T& overload in terms of the T&& one like so:
template <typename T> |
how a modern C++ compiler is supposed to handle return-by-value (slightly simplified, as always):
In a return statement of the form return name;, a compiler is obliged to treat name as if it were an rvalue expression, provided name is either the name of a locally defined automatic variable or that of a function parameter.
In a return statement of the form return name;, a compiler is allowed to apply the so-called named return value optimization (NRVO), provided name is the name of a locally defined automatic variable (so not if it is that of a function parameter).
The first bullet implies that using std::move(result) in our example would be, at the very least, redundant. Even
without the std::move(), the compiler already treats result as if it is an rvalue. The second bullet moreover implies that return std::move(result) would prohibit the NRVO optimization. NRVO applies solely to statements of the form return result;
if the variable value in return value; has static or thread-local storage duration (see Chapter 11), you need to add std::move() if moving is what you want. This case is rare, though. When returning an object’s member variable, as in return m_member_variable;, std::move() is again required if you do not want the member variable to be copied. If the return statement contains any other lvalue expression besides the name of a single variable, then NRVO does not apply, nor will the compiler treat this lvalue as if it were an rvalue when looking for a constructor.
Array<T>& Array<T>::operator=(Array&& rhs) noexcept { |
19. First-Class Functions
lambda
auto less{[](int x, int y) { return x < y; }}; |
in []
capture by value : =, or just para name
capture by ref : &
specific :
auto counter{ [&count](int x, int y)
{ ++count; return x < y; }
};Here, count is the only variable in the enclosing scope that can be accessed from within the body of the lambda.
The capture default, if used, should always come first. Capture clauses such as [&counter, =] or [number_to_search_for, &] are therefore not allowed.
If you use the = capture default, you are no longer allowed to capture any specific variables by value; similarly, if you use &, you can no longer capture specific variables by reference. Capture clauses such as [&, &counter] or [=, &counter, number_to_search_for] are therefore not allowed.