記事ディレクトリ
1. C++11 の概要
C++11 は C++ 言語のメジャー アップデート バージョンです。2011 年にリリースされました。これには非常に便利な新機能がいくつか含まれており、開発者に優れたプログラミング ツールと優れたプログラミング エクスペリエンスを提供し、効率的かつ信頼性の高いコードの作成を可能にします。のほうが簡単です。
C++11 の新機能には次のようなものがあります。
- 強制型列挙により、列挙型の通常の動作の信頼性が高まり、制御が容易になります。
- 自動型推論 (auto) では、実際の条件に基づいて変数の型を自動的に推論できるため、コードがより簡潔で読みやすくなります。
- 匿名関数をサポートするラムダ式は、関数オブジェクトを定義して使用するためのシンプルかつ強力な方法を提供します。
- グローバル関数の型推論により、関数の戻り値の型を自動的に推論して、定義の繰り返しを回避できます。
- 右辺値参照は、プログラムのパフォーマンスと効率を向上させると同時に、移動セマンティクスなどの高度なプログラミング手法をより適切に実装できます。
- マルチスレッド プログラムをより簡単かつ安全に作成できるように設計された同時実行ライブラリ。
さらに、C++11 では、スマート ポインター、デフォルトの削除関数、明示的な変換演算子、間隔反復子など、他の多くの新機能も導入されています。
C++0x から C++11 まで、C++ 標準は 10 年間にわたって研削されてきましたが、2 番目の真の標準は遅れて登場しました。C++98/03 と比較して、C++11 には、C++03 標準の約 140 の新機能や約 600 の欠陥の修正など、かなりの数の変更が加えられています。 C++98/03から生まれた新しい言語。比較すると、C++11 はシステム開発やライブラリ開発に適しており、構文がより一般的で簡素化され、より安定して安全であり、機能がより強力であるだけでなく、プログラマの開発効率も向上します。企業の実際のプロジェクト開発でもよく使われるので、重点的に勉強しておきましょう。
全体として、C++11 は C++ 言語の大幅な改善であり、より優れたプログラミング ツールと、より効率的で読みやすく保守しやすいコード作成方法を提供するため、C++11 を学習することは生涯にわたるメリットとなります。
ショートストーリー:
1998 年は、C++ 標準委員会が設立された最初の年でした。当初は、実際のニーズに基づいて 5 年ごとに標準を更新する予定でした。C++ 国際標準委員会が C++03 の次のバージョンを検討していたとき、当初は、 2007 年にリリースされたため、最初はこの標準は C++07 と呼ばれていました。しかし、2006 年までに関係者は、C++07 は 2007 年には絶対に完成しない、また 2008 年には完成しないかもしれないと感じていました。最終的には、単に C++ 0x と呼ばれるようになりました。× は、2007 年、2008 年、または 2009 年に完成するかどうかわからないことを意味します。その結果、2010 年には完成せず、C++ 標準がようやく完成したのは 2011 年でした。そこで最終的には C++11 と名付けられました。(Java はこの点において非常に熱心です)
2. 均一リストの初期化
2.1 {} の初期化
C++98 では、標準により、配列または構造要素の均一なリスト初期化に中かっこ {} の使用が許可されています。例えば:
struct Point { int _x; int _y; }; int main() { int array1[] = { 1, 2, 3, 4, 5 }; int array2[5] = { 0 }; Point p = { 1, 2 }; return 0; }
C++11 では、中括弧で囲まれたリスト (初期化リスト) の使用が拡張され、すべての組み込み型とユーザー定義型に使用できるようになりました。初期化リストを使用する場合は、等号 (=) を追加できます。または追加しないでください。
struct Point { int _x; int _y; }; int main() { int x1 = 1; int x2{ 2 }; int array1[]{ 1, 2, 3, 4, 5 }; int array2[5]{ 0 }; Point p{ 1, 2 }; // C++11中列表初始化也可以适用于new表达式中 int* pa = new int[4]{ 0 }; return 0; }
もちろん、オブジェクトを作成するときに、リストの初期化を使用してコンストラクターの初期化を呼び出すこともできます。
class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout << "Date(int year, int month, int day)" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 1, 1); // C++11支持的列表初始化,这里会调用构造函数初始化 Date d2{ 2022, 1, 2 }; Date d3 = { 2022, 1, 3 }; return 0; }
2.2 std::initializer_list
std::initializer_list の入門ドキュメント
std::initializer_list の型は次のとおりです。
int main() { auto il = { 1,3,2,5 }; cout << typeid(il).name() << endl;//class std::initializer_list<int> return 0; }
std::initializer_list の使用シナリオ:
std::initializer_list は通常、コンストラクターのパラメーターとして使用されますが、C++11 では、STL の多くのコンテナーのパラメーター コンストラクターとして std::initializer_list が追加され、コンテナー オブジェクトの初期化がより便利になります。また、operator= のパラメータとしても使用できるため、中括弧を使用して値を割り当てることができます。
以下は、一部のコンテナーのコンストラクターのドキュメントの紹介です。ほとんどのコンテナーは std::initializer_list コンストラクトをサポートしています。
これはマップコンテナにとって非常に便利です
例えば:
int main() { vector<int> v = { 1,2,3,4 }; list<int> lt = { 1,2 }; // 这里{"sort", "排序"}会先初始化构造一个pair对象 map<string, string> dict = { { "sort", "排序"}, { "insert", "插入"} }; // 使用大括号对容器赋值 v = { 10, 20, 30 }; return 0; }
シミュレートされたベクトルが {} の初期化と割り当てもサポートできるようにします
vector(std::initializer_list<T> il) :_start(nullptr) , _finish(nullptr) , _end_of_storage(nullptr) { for (const auto& e : il) { push_back(e); } }
3. 声明
C++11 では、特にテンプレートを使用する場合に、宣言を簡素化するさまざまな方法が提供されています。
3.1 自動
C++98 では、auto はストレージ型指定子であり、変数がローカルの自動ストレージ型であることを示しますが、ローカル ドメインで定義されたローカル変数はデフォルトで自動ストレージ型になるため、auto には値がありません。auto の本来の使用法は C++11 では廃止され、自動型判定の実装に使用されます。これには明示的な初期化が必要です。これにより、コンパイラは定義オブジェクトの型を初期化値の型に設定します。
int main() { int i = 10; auto p = &i;// 推导为int*类型 auto pf = "apple";//推导为const char* 类型 cout << typeid(p).name() << endl; cout << typeid(pf).name() << endl; map<string, string> dict = { { "apple", "苹果"}, { "banana", "香蕉"} }; //map<string, string>::iterator it = dict.begin(); auto it = dict.begin(); return 0; }
3.2 decltype
キーワード decltype は、変数の型が式で指定された型になるように宣言します。
// decltype的一些使用使用场景 template<class T1, class T2> void F(T1 t1, T2 t2) { decltype(t1 * t2) ret; cout << typeid(ret).name() << endl; } int main() { const int x = 1; double y = 2.2; decltype(x * y) ret; // ret的类型是double decltype(&x) p; // p的类型是int* cout << typeid(ret).name() << endl; cout << typeid(p).name() << endl; F(1, 'a');//int 和 char --》int return 0; }
3.3 auto と decltype の違い
decltype と auto は、C++11 で提供される 2 つの新しいキーワードであり、これらの機能は、コンパイラに変数の型を自動的に推測させることです。
auto は、ローカル変数と関数の戻り値の自動型推定に使用でき、コンパイラは式の型に基づいて変数の型を推定します。例えば:
auto i = 10; // 推导为int类型 auto s = "hello"; // 推导为const char*类型
decltype は、変数や式の戻り値の型を含む式の型を取得するために使用されます。例えば:
int i = 10; decltype(i) j; // 推导类型为int double getValue(); decltype(getValue()) d; // 推导类型为double
ご覧のとおり、decltype を使用する場合は式または変数名を指定する必要がありますが、auto では指定しません。さらに、decltype の戻り値の型は、const、reference、cv 修飾子などを含む完全に正確な型を持ちますが、auto はネイキッド型のみを推定でき、完全な型を推定するには型導出と定数修飾子が必要です。
つまり、decltype と auto はどちらも変数の型を推定するために使用できますが、その機能は少し異なります。Auto は主にローカル変数と関数の戻り値の自動型推論に使用され、decltype は const、reference、その他の修飾子を含む式の正確な型を取得するために使用されます。
3.4 nullptr
C++ では NULL はリテラル 0 として定義されているため、0 はポインター定数と整数定数の両方を表すことができるため、問題が発生する可能性があります。したがって、明確さと安全性のために、C++11 では null ポインターを表す nullptr が追加されています。
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
4. 参照の右辺値と移動セマンティクス
4.1 左辺値参照と右辺値参照
従来の C++ 構文には参照構文があり、C++11 では新しい右辺値参照構文機能が追加されたため、以前に学習した参照は今後左辺値参照と呼ばれます。左辺値参照か右辺値参照かに関係なく、オブジェクトには別名が与えられます。
では、左辺値とは正確には何でしょうか? 右辺値とは何ですか? 左辺値参照とは何ですか? 右辺値参照とは何ですか?
左辺値これはデータを表す式です (変数名や逆参照ポインタなど)。そのアドレスを取得し、それに値を割り当てることができます。左辺値は代入記号の左側に表示できますが、右辺値は指定できません割り当て記号の左側に表示されます。const 修飾子の定義時に後の lvalue には値を割り当てることはできませんが、そのアドレスを取得することはできます。左辺値参照は左辺値への参照であり、左辺値には別名が与えられます。
int main() { // 以下的p、b、c、*p都是左值 int* p = new int(0); int b = 1; const int c = 2; // 以下几个是对上面左值的左值引用 int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p; return 0; }
右辺値これは、リテラル定数、式の戻り値、関数の戻り値 (左辺値参照の戻り値にすることはできません) などのデータを表す式でもあります。右辺値は代入記号の右側に表示できます。ただし、代入記号には表示できません。左側の右辺値はアドレスを取ることができません。右辺値参照は右辺値への参照であり、右辺値に別名を与えます。
int main() { double x = 1.1, y = 2.2; // 以下几个都是常见的右值 10; x + y; fmin(x, y); // 以下几个都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = fmin(x, y); // 这里编译会报错:error C2106: “=”: 左操作数必须为左值 10 = 1; x + y = 1; fmin(x, y) = 1; return 0; }
右辺値のアドレスを取得することはできませんが、右辺値に別名を与えると、右辺値が特定の場所に格納され、その場所のアドレスを取得できることに注意してください。 : リテラル 10 は取得できませんが、rr1 を参照すると、rr1 のアドレスを取得したり、rr1 を変更したりできます。rr1 を変更したくない場合は、 const int&& rr1 を使用して参照できます。素晴らしいと思いませんか? 右辺値参照の実際の使用シナリオはこれに当てはまらず、この機能は重要ではないことを理解しましょう。
int main() { double x = 1.1, y = 2.2; int&& rr1 = 10; const double&& rr2 = x + y; rr1 = 20; rr2 = 5.5; // 报错 return 0; }
4.2 左辺値参照と右辺値参照の比較
左辺値参照の概要:
左辺値参照は左辺値のみを参照でき、右辺値は参照できません。
ただし、const lvalue 参照は、lvalue と rvalue の両方を参照できます。
int main() { // 左值引用只能引用左值,不能引用右值。 int a = 10; int& ra1 = a; // ra为a的别名 //int& ra2 = 10; // 编译失败,因为10是右值 // const左值引用既可引用左值,也可引用右值。 const int& ra3 = 10; const int& ra4 = a; return 0; }
右辺値リファレンスの概要:
右辺値参照は右辺値のみを参照でき、左辺値は参照できません。
ただし、右辺値参照は後の左辺値に移動できます。
int main() { // 右值引用只能右值,不能引用左值。 int&& r1 = 10; // error C2440: “初始化”: 无法从“int”转换为“int &&” // message : 无法将左值绑定到右值引用 int a = 10; int&& r2 = a;//报错:“初始化”: 无法从“int”转换为“int &&” // 右值引用可以引用move以后的左值 int&& r3 = std::move(a); return 0; }
4.3 使用シナリオと右辺値参照の重要性
左辺値参照が左辺値と右辺値の両方を参照できることは前にわかりましたが、なぜ C++11 も右辺値参照を提案するのでしょうか? 余計なことですか?左辺値参照の欠点と、右辺値参照がこれらの欠点をどのように補うことができるかを見てみましょう。
左辺値参照の使用シナリオ:
パラメーターの作成と戻り値の作成の両方で効率が向上します。void func1(hdm::string s) { } void func2(const hdm::string& s) { } int main() { hdm::string s1("hello world"); // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值 func1(s1); func2(s1); // string operator+=(char ch) 传值返回存在深拷贝 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率 s1 += '!'; return 0; }
注: 印刷を有効にするには、独自のシミュレートされた文字列を使用する必要があります。ディープ コピー時に入力コードを追加するだけです。次の例も同様です。
//现代写法 string& operator=(string s) { swap(s); cout << "string& operator=(string s) --- 深拷贝" << endl; return *this; }
左辺値参照の欠点:
ただし、関数によって返されるオブジェクトがローカル変数の場合、そのオブジェクトは関数のスコープ外に存在しないため、左辺値参照によって返すことはできず、値によってのみ返すことができます。例: hdm::string to_string(int value) 関数でわかるように、ここでは値による戻りのみが使用でき、値による戻りでは少なくとも 1 つのコピー構築が発生します (古いコンパイラの場合は、 2 つのコピー構造である必要があります)。
右辺値参照と移動セマンティクスは、上記の問題を解決します。
move コンストラクトを hdm::string に追加します。moveコンストラクトの本質は、パラメーターの右辺値のリソースを盗むことです。プレースホルダーがすでに存在する場合、ディープ コピーを行う必要はありません。したがって、これは move と呼ばれます構築、つまり、他の人のリソースを盗んで自分のリソースを構築することを意味します。
string(string&& s) :_str(nullptr), _capacity(0), _size(0) { cout << "string(string&& s)---移动构造" << endl; swap(s); }
次に、上記の hdm::to_string の 2 つの呼び出しを実行すると、ここではディープ コピーのコピー構造が呼び出されず、ムーブ構造が呼び出されていることがわかります。ムーブ構造には新しいスペースがなく、データがコピーされます。したがって効率が向上します。
建設を移動するだけでなく、割り当ても移動します。
bit::string クラスに移動代入関数を追加して hdm::to_string(1234) を呼び出しますが、今回は hdm::to_string(1234) によって返される右辺値オブジェクトが ret1 オブジェクトに代入されます。呼び出しはモバイル構造です
string& operator=(string&& s) { cout << "string operator=(string&& s)---移动赋值" << endl; swap(s); return *this; } int main() { hdm::string ret1; ret1 = hdm::to_string(1234); return 0; } // 运行结果: // string(string&& s) -- 移动构造 // string& operator=(string&& s) -- 移动赋值
これを実行すると、移動コンストラクターと移動代入が呼び出されていることがわかります。既存のオブジェクトを使用して受信すると、コンパイラーはそれを最適化できないためです。hdm::to_string 関数は、最初に str 生成構造を使用して一時オブジェクトを生成しますが、コンパイラーは str を右辺値として認識し、move 構造を呼び出すのに十分賢いことがわかります。次に、この一時オブジェクトを hdm::to_string 関数呼び出しの戻り値として ret1 に割り当て、ここで呼び出される移動代入を行います。
次の場合にのみ、コンパイラはそれを移動構造に直接最適化します。
int main() { hdm::string ret1 = hdm::to_string(1234); return 0; } // 运行结果: // string(string&& s) -- 移动构造
4.4 rvalue リファレンスは、lvalue と使用シナリオのより詳細な分析を指します。
構文によれば、右辺値参照は右辺値のみを参照できますが、右辺値参照は左辺値を参照してはなりませんか?
理由: 一部のシナリオでは、移動セマンティクスを実装するために、右辺値を使用して左辺値を参照することが実際に必要になる場合があります。左辺値を参照するために右辺値参照を使用する必要がある場合は、move 関数を使用して左辺値を右辺値に変換できます。C++11 では、std::move() 関数はヘッダー ファイルにあります。この関数の名前は紛らわしいです。何も移動しません。その唯一の機能は、左辺値を右辺値参照に強制して実装することです。動き、セマンティクス。
int main() { hdm::string s1("hello world"); // 这里s1是左值,调用的是拷贝构造 hdm::string s2(s1); // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造 // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的 // 资源被转移给了s3,s1被置空了。 hdm::string s3(std::move(s1)); return 0; }
STL コンテナ挿入インターフェイス関数は、右辺値参照バージョンも追加します:
STL—リスト ドキュメントint main() { list<hdm::string> lt; hdm::string s1("1111"); // 这里调用的是拷贝构造 lt.push_back(s1); // 下面调用都是移动构造 lt.push_back("2222"); lt.push_back(std::move(s1)); return 0; }
4.5 完全転送
&& テンプレート内のユニバーサル参照
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } // 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。 // 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力, // 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值, // 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发 template<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
正しくは、出力内容は上記のコメントに書かれた内容であるはずですが、実際はそうではありません。理由は、パラメーターの受け渡し処理で t に渡されたオブジェクトが、その後 Fun が呼び出されたときに左辺値になっているためです。 t に渡されます。t が値を受け取った後、t のオブジェクトが t 値に格納され、すべての t が左辺値になります。
解決策: std::forward、完全転送を使用して、パラメーター転送プロセス中にオブジェクトのネイティブ型属性を保持します。
template<typename T> void PerfectForward(T&& t) { // std::forward<T>(t)在传参的过程中保持了t的原生类型属性 Fun(std::forward<T>(t)); }
完璧な転送の実際の使用シナリオ:
template<class T> struct ListNode { ListNode* _next = nullptr; ListNode* _prev = nullptr; T _data; }; template<class T> class List { typedef ListNode<T> Node; public: List() { _head = new Node; _head->_next = _head; _head->_prev = _head; } void PushBack(T&& x) { //Insert(_head, x); Insert(_head, std::forward<T>(x)); } void PushFront(T&& x) { //Insert(_head->_next, x); Insert(_head->_next, std::forward<T>(x)); } void Insert(Node* pos, T&& x) { Node* prev = pos->_prev; Node* newnode = new Node; newnode->_data = std::forward<T>(x); // 关键位置 // prev newnode pos prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } void Insert(Node* pos, const T& x) { Node* prev = pos->_prev; Node* newnode = new Node; newnode->_data = x; // 关键位置 // prev newnode pos prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } private: Node* _head; }; int main() { List<hdm::string> lt; lt.PushBack("1111"); lt.PushFront("2222"); return 0; } //运行结果 //string operator=(string&& s)---移动赋值 //string operator=(string&& s)-- - 移动赋值
5 つの新しいクラス関数
デフォルトのメンバー関数
元の C++ クラスには 6 つのデフォルトのメンバー関数があります。
コンストラクタ
デストラクタ
コピーコンストラクター
コピー代入のオーバーロード
アドレスのオーバーロードを取る
const address のオーバーロード
最後に重要なのは最初の 4 つで、最後の 2 つはあまり役に立ちません。デフォルトのメンバー関数は、記述しない場合にコンパイラーが生成するデフォルトの関数です。C++11 では、移動コンストラクターと移動代入演算子のオーバーロードという 2 つの新しいオーバーロードが追加されています。移動コンストラクターと移動代入演算子のオーバーロードについては、次のような注意すべき点がいくつかあります。
移動コンストラクターを自分で実装せず、デストラクター、コピー構築、およびコピー代入のオーバーロードを実装しない場合。その後、コンパイラはデフォルトの移動コンストラクターを自動的に生成します。デフォルトで生成される移動コンストラクターは、組み込み型メンバーに対してメンバーごと、バイトごとのコピーを実行します。カスタム型メンバーの場合、メンバーが移動構築を実装しているかどうかを確認する必要があります。実装されている場合、移動コンストラクターはそうでない場合は、コピー コンストラクターが呼び出されます。
移動代入オーバーロード関数を自分で実装せず、デストラクター、コピー構築、およびコピー代入オーバーロードのいずれも実装しない場合、コンパイラーはデフォルトの移動代入を自動的に生成します。デフォルトで生成される移動コンストラクタは、組み込み型のメンバーに対してメンバーごとにバイトごとのコピーを実行します。カスタム型のメンバーについては、メンバーが移動割り当てを実装しているかどうかを確認する必要があります。実装している場合は、移動割り当てを呼び出します。そうでない場合は、コピー代入を呼び出します。(デフォルトの移動割り当ては、上記の移動構造と完全に似ています)
移動構築または移動代入を指定した場合、コンパイラーはコピー構築およびコピー代入を自動的には提供しません。
// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。 class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) { } //-----------------------------------// //如果没有自己实现下面任意一个,那么编译器就会帮我们默认生成一个移动构造和移动赋值 /*Person(const Person& p) :_name(p._name) ,_age(p._age) {}*/ /*Person& operator=(const Person& p) { if(this != &p) { _name = p._name; _age = p._age; } return *this; }*/ /*~Person() {}*/ //-----------------------------------// private: hdm::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); Person s4; s4 = std::move(s2); return 0; } //运行结果: //string(const string& s)---深拷贝 //string(string&& s)---移动构造 //string operator=(string&& s)---移动赋值
キーワード default を使用すると、デフォルト関数の生成が強制されます。
C++11 では、どのデフォルト関数を使用するかをより詳細に制御できます。デフォルトの関数を使用したいが、何らかの理由でこの関数がデフォルトでは生成されないとします。たとえば、コピー構造を指定した場合、移動構造は生成されません。その場合は、default キーワードを使用して、指定された移動構造の生成を表示できます。
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) { } Person(const Person& p) :_name(p._name) , _age(p._age) { } Person(Person&& p) = default; private: hdm::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1);//使用了默认生成的移动构造 return 0; } //运行结果 //string(const string& s)-- - 深拷贝 //string(string && s)-- - 移动构造
デフォルト関数の生成を禁止するキーワード削除:
一部のデフォルト関数の生成を制限したい場合、C++98 では、その関数はプライベートに設定され、宣言されるだけで定義されないため、他の関数がその関数を呼び出したい限りエラーが報告されます。C++11 ではより単純で、関数宣言に =delete を追加するだけです。この構文は、対応する関数のデフォルト バージョンを生成しないようにコンパイラーに指示し、=delete によって変更された関数は削除関数と呼ばれます。
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) { } Person(const Person& p) = delete; private: hdm::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; } //运行报错 //error C2280: “Person::Person(const Person &)”: 尝试引用已删除的函数
6. 可変個引数テンプレート
C++11 の新しい機能の変数パラメーター テンプレートを使用すると、変数パラメーターを受け入れることができる関数テンプレートとクラス テンプレートを作成できます。C++98/03 と比較して、クラス テンプレートと関数テンプレートには、固定数のテンプレート パラメーターのみを含めることができます。テンプレートパラメータは間違いなく大幅な改善です。ただし、変数テンプレート パラメーターは比較的抽象的であり、使用するには特定のスキルが必要であるため、この知識はまだ比較的曖昧です。この段階では、いくつかの基本的な変数パラメーター テンプレート機能をマスターするだけで十分です。ここで終了します。必要に応じて、さらに詳しく学習できます。
以下は基本的な変数パラメータ関数テンプレートです。
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) { }
上記のパラメータ args の前に省略記号があるため、これは可変テンプレート パラメータです。省略記号の付いたパラメータを「パラメータ パック」と呼び、0 ~ N (N>=0) のテンプレート パラメータが含まれます。パラメータパック args 内の各パラメータを直接取得することはできず、パラメータ パックを展開することによってのみパラメータ パック内の各パラメータを取得することができます。これが可変テンプレート パラメータを使用する主な特徴であり、最大の難点でもあります。変数テンプレートパラメータを展開します。構文は args[i] を使用して変数パラメーターを取得することをサポートしていないため、いくつかの奇妙なトリックを使用してパラメーター パッケージの値を 1 つずつ取得します。
再帰関数モードでのパラメータパックの拡張
template<class T> void showList(T val) { cout << val << endl; } template<class T,class ...Args> void showList(T value, Args... args) { cout << value << " "; showList(args...); } int main() { showList(1); showList(1,'a'); showList(1,'a',"string"); return 0; }
カンマ式拡張パラメータパック
パラメータ パックを展開するこの方法では、再帰によって関数を終了する必要はありません。expand 関数本体で直接展開されます。printarg は再帰終了関数ではなく、パラメータ パック内の各パラメータを処理する関数です。このパラメータ パッケージのインプレース展開の鍵となるのは、カンマ式です。カンマ式では、カンマの前にある式が順番に実行されることがわかっています。Expand 関数のカンマ式: (printarg(args), 0) もこの実行シーケンスに従い、最初に printarg(args) を実行してから、カンマ式の結果 0 を取得します。同時に、C++11 初期化リストの別の機能が使用されます。初期化リストを通じて可変長配列を初期化するには、{(printarg(args), 0)...} は ((printarg) に展開されます(arg1), 0 )、(printarg(arg2),0)、(printarg(arg3),0)、etc… ) は、最終的に要素値がすべてである配列 int arr[sizeof… (Args)] を作成します。 0. これはカンマ式であるため、配列の作成プロセスで、カンマ式の前の printarg(args) が最初に実行されて
パラメータが出力されます。つまり、パラメータ パックは、配列の構築中に展開されます。 int 配列 この配列の目的は、純粋に配列構築プロセス中にパラメータ パッケージを展開することです。template<class T> void PrintArg(T value) { cout << value << " "; } template<class ...Args> void showList( Args... args) { int arr[] = { (PrintArg(args),0)... }; cout << endl; } int main() { showList(1000); showList(1000,'a'); showList(1000,'a',"string"); return 0; }
7. ラムダ式
7.1 C++98 での例
C++98 では、データ コレクション内の要素を並べ替える場合は、std::sort メソッドを使用できます。
#include <algorithm> #include <functional> void PrintArr(int arr[],int size){ for (int i=0;i<size;++i){ cout << arr[i] << " "; } cout << endl; } int main(){ int arr[] = { 3,2,1,5,6,4,7,8,0 }; // 默认按照小于比较,排出来结果是升序 std::sort(arr, arr + sizeof(arr) / sizeof(arr[0])); PrintArr(arr, sizeof(arr) / sizeof(arr[0])); // 如果需要降序,需要改变元素的比较规则 std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]), greater<int>()); PrintArr(arr, sizeof(arr) / sizeof(arr[0])); return 0; } //运行结果 //0 1 2 3 4 5 6 7 8 //8 7 6 5 4 3 2 1 0
並べ替える要素がカスタム タイプである場合、ユーザーは並べ替えのための比較ルールを定義する必要があります。
struct Goods{ string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) { } }; struct ComparePriceLess//比较方式的仿函数{ bool operator()(const Goods& gl, const Goods& gr){ return gl._price < gr._price; } }; struct ComparePriceGreater{ bool operator()(const Goods& gl, const Goods& gr){ return gl._price > gr._price; } }; int main(){ vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } }; sort(v.begin(), v.end(), ComparePriceLess()); sort(v.begin(), v.end(), ComparePriceGreater()); return 0; }
C++ 構文の発展に伴い、上記の記述方法は複雑すぎると感じるようになり、アルゴリズムを実装するには毎回新しいクラスを作成する必要があり、比較のロジックが毎回異なると、複数のクラスが特に、同じクラスの名前付けはプログラマにとって非常に迷惑です。したがって、ラムダ式は C++11 構文に登場しました。
7.2 ラムダ式
デモンストレーション
int main(){ vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } }; //根据价格排升序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; }); //根据价格排降序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; }); //根据评价排升序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate < g2._evaluate; }); //根据评价排降序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate > g2._evaluate; }); return 0; }
上記のコードは C++11 のラムダ式を使用して解決されており、ラムダ式が実際には匿名関数であることがわかります。
7.3 ラムダ式の構文
ラムダ式記述形式: [キャプチャリスト] (パラメータ) mutable -> return-type {statement}
- ラムダ式の各部の説明
- [capture-list]:ラムダ関数の先頭に常に現れるキャプチャ リスト。コンパイラは [] に従って次のコードがラムダ関数であるかどうかを判断し、キャプチャ リストはコンテキスト内の変数をキャプチャして使用できます。ラムダ関数による。
- (パラメータ): パラメータのリスト。通常の関数のパラメータリストと一致し、パラメータの受け渡しが必要ない場合は、() を使用して省略できます。
- mutable: デフォルトでは、ラムダ関数は常に const 関数であり、mutable はその定数性をキャンセルできます。この修飾子を使用する場合、パラメータ リストを省略することはできません (パラメータが空の場合でも)。
- ->returntype: 戻り値の型。戻り値追跡フォームを使用して、関数の戻り値の型を宣言します。戻り値がない場合、この部分は省略できます。戻り値の型が明確な場合は省略することもでき、コンパイラは戻り値の型を推測します。
- {ステートメント}: 関数本体。関数の本体内では、パラメーターに加えて、キャプチャされたすべての変数を使用できます。
注:
ラムダ関数定義では、パラメーター リストと戻り値の型はオプションの部分であり、キャプチャ リストと関数本体は空にすることができます。したがって、C++11 の最も単純なラムダ関数は次のようになります: []{}; このラムダ関数は何も実行できません。int main(){ // 最简单的lambda表达式, 该lambda表达式没有任何意义 [] { }; // 省略参数列表和返回值类型,返回值类型由编译器推导为int int a = 3, b = 4; [=] { return a + 3; }; // 省略了返回值类型,无返回值类型 auto fun1 = [&](int c) { b = a + c; }; fun1(10); cout << a << " " << b << endl; // 相对完善的lambda函数 auto fun2 = [=, &b](int c)->int { return b += a + c; }; cout << fun2(10) << endl; // 复制捕捉x int x = 10; auto add_x = [x](int a) mutable { x *= 2; return a + x; }; cout << add_x(10) << endl; return 0; }
- キャプチャリストの説明
キャプチャ リストには、コンテキスト内のどのデータがラムダで使用できるか、およびそのデータが値で渡されるか参照で渡されるかが記述されます。
- [var]: 変数varをキャプチャするための値の転送方法を示します。
- [=]: 値渡しメソッドが親スコープ内のすべての変数 (これを含む) をキャプチャすることを示します。
- [&var]: キャプチャ変数 var が参照によって渡されることを示します。
- [&]: 参照転送が親スコープ内のすべての変数 (これを含む) をキャプチャすることを示します。
- [this]: 値転送メソッドが現在の this ポインタをキャプチャすることを示します。
知らせ:
a. 親スコープは、ラムダ関数を含むステートメント ブロックを参照します。
b. 構文的には、キャプチャ リストはカンマで区切られた複数のキャプチャ アイテムで構成されます。
例: [=, &a, &b]: 変数 a と b を参照によってキャプチャし、他のすべての変数を値によってキャプチャします [&, a, this]: 変数 a と this を値によってキャプチャし、他の変数を参照によってキャプチャします
。
- C キャプチャ リストでは、変数を繰り返し渡すことはできません。そうしないと、コンパイル エラーが発生します。
例: [=, a]: = は値転送によってすべての変数をキャプチャし、繰り返しキャプチャします。
dブロックスコープ外のラムダ関数のキャプチャリストは空でなければなりません。
ブロック スコープ内の e のラムダ関数は、親スコープ内のローカル変数のみをキャプチャできるため、スコープ外または
ローカル以外の変数をキャプチャするとコンパイル エラーが発生します。ラムダ式は、同じ型であるように見えても、相互に割り当てることはできません。
void (*PF)(); int main(){ auto f1 = [] { cout << "hello world" << endl; }; auto f2 = [] { cout << "hello world" << endl; }; //f1 = f2; // 编译失败--->提示找不到operator=() // 由于 Lambda 表达式是匿名的,因此不能直接将一个 Lambda 对象赋值给另一个 Lambda 对象。这是因为 Lambda 表达式没有默认的拷贝构造函数或赋值运算符重载,因此无法像普通的对象一样进行拷贝或赋值。 // 允许使用一个lambda表达式拷贝构造一个新的副本 auto f3(f2); f3(); // 可以将lambda表达式赋值给相同类型的函数指针 PF = f2; PF(); return 0; }
7.4 関数オブジェクトとラムダ式
ファンクターとも呼ばれる関数オブジェクトは、関数のように使用できるオブジェクトで、クラス内の Operator() 演算子をオーバーロードするクラス オブジェクトです。
class Rate{ public: Rate(double rate) : _rate(rate){ } double operator()(double money, int year){ return money * _rate * year; } private: double _rate; }; int main(){ // 函数对象 double rate = 0.5; Rate r1(rate); cout << r1(10000, 2) << endl; // lamber auto r2 = [=](double monty, int year)->double { return monty * rate * year;}; cout << r2(10000, 2) << endl; return 0; } //运行结果 //10000 //10000
使用法に関しては、関数オブジェクトはラムダ式とまったく同じです。
関数オブジェクトはメンバー変数として rate を持ち、オブジェクトの定義時に初期値を与えることができ、ラムダ式はキャプチャ リストを通じて変数を直接キャプチャできます。
実際、基礎となるコンパイラは、ラムダ式を関数オブジェクトとして正確に処理します。つまり、ラムダ式が定義されている場合、コンパイラは、operator( ) を含むクラスを自動的に生成します。
8. ラッパー
関数ラッパー
関数ラッパーはアダプターとも呼ばれます。C++ の関数は本質的にクラス テンプレートとラッパーです。
それでは、なぜ関数が必要なのかを見てみましょう。//ret = func(x); // 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能 //是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下! //为什么呢?我们继续往下看 template<class F, class T> T useF(F f, T x){ static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i){ return i / 2; } struct Functor{ double operator()(double d){ return d / 3; } }; int main(){ // 函数名 cout << useF(f, 11.11) << endl; // 函数对象 cout << useF(Functor(), 11.11) << endl; // lamber表达式 cout << useF([](double d)->double { return d / 4; }, 11.11) << endl; return 0; }
上記のプログラム検証により、useF 関数テンプレートが 3 回インスタンス化されていることがわかります。
ラッパーは上記の問題をうまく解決できます
std::function在头文件<functional> // 类模板原型如下 template <class T> function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>; 模板参数说明: Ret: 被调用函数的返回类型 Args…:被调用函数的形参
// 使用方法如下: #include <functional> int f(int a, int b){ return a + b; } struct Functor{ public: int operator() (int a, int b){ return a + b; } }; class Plus{ public: static int plusi(int a, int b){ return a + b; } double plusd(double a, double b){ return a + b; } }; using func_t = std::function<int(int, int)>; int main(){ // 函数名(函数指针) func_t func1 = f; cout << func1(1, 2) << endl; // 函数对象 func_t func2 = Functor(); cout << func2(1, 2) << endl; // lamber表达式 func_t func3 = [](const int a, const int b) { return a + b; }; cout << func3(1, 2) << endl; // 类的成员函数 func_t func4 = &Plus::plusi; cout << func4(1, 2) << endl; std::function<double(Plus, double, double)> func5 = &Plus::plusd; cout << func5(Plus(), 1.1, 2.2) << endl; return 0; } //运行结果 //3 //3 //3 //3 //3.3
ラッパーを使用して、テンプレート効率の低さと複数のインスタンス化の問題をどのように解決すればよいでしょうか?
#include <functional> template<class F, class T> T useF(F f, T x){ static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i){ return i / 2; } struct Functor{ double operator()(double d){ return d / 3; } }; int main(){ // 函数名 std::function<double(double)> func1 = f; cout << useF(func1, 11.11) << endl; // 函数对象 std::function<double(double)> func2 = Functor(); cout << useF(func2, 11.11) << endl; // lamber表达式 std::function<double(double)> func3 = [](double d)->double { return d / 4; }; cout << useF(func3, 11.11) << endl; return 0; }
練る
std::bind 関数はヘッダー ファイルで定義され、関数テンプレートです。関数ラッパー (アダプター) のようなもので、呼び出し可能なオブジェクトを受け入れ、元のオブジェクトを「適応」する新しい呼び出し可能なオブジェクトを生成します。パラメーターのリスト。一般的に言えば、これを使用して、元々 N 個のパラメータを受け取った関数 fn を変換し、いくつかのパラメータをバインドすることで、M (M は N より大きくても構いませんが、これには意味がありません) パラメータを受け取る新しい関数を返すことができます。同時に、std::bind関数を使用することでパラメータの順序調整などの操作も実装できます。
// 原型如下: template <class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args); // with return type (2) template <class Ret, class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
バインド関数は、呼び出し可能オブジェクトを受け取り、元のオブジェクトの引数リストに「適合する」新しい呼び出し可能オブジェクトを生成する汎用関数アダプターと考えてください。
binding 呼び出しの一般的な形式: auto newCallable = binding(callable, arg_list); このうち、newCallable 自体は呼び出し可能なオブジェクトであり、arg_list は指定された呼び出し可能なパラメーターに対応するカンマ区切りのパラメーター リストです。newCallable を呼び出すと、newCallable は callable を呼び出し、arg_list 内のパラメータを渡します。
arg_list のパラメータには、_n という形式の名前が含まれる場合があります。n は整数です。これらのパラメータは、newCallable のパラメータを表す「プレースホルダ」であり、newCallable に渡されるパラメータの「位置」を占めます。値 n は、生成された呼び出し可能オブジェクト内のパラメーターの位置を表します。_1 は newCallable の最初のパラメーター、_2 は 2 番目のパラメーターなどです。void Print(const char* str, int value) { cout << str << value << endl; } int main() { const char* str = "bind---"; int value = 1; Print(str, value);//正常用法 auto func1 = std::bind(Print, str, std::placeholders::_1);//绑定一个参数 func1(value + 1); auto func2 = std::bind(Print, str, value+2);//绑定两个参数 func2(); return 0; } //运行结果 //bind-- - 1 //bind-- - 2 //bind-- - 3
9. スレッドライブラリ
9.1 スレッドクラスの簡単な紹介
C++11 より前は、マルチスレッドの問題はすべてプラットフォームに関連していました。たとえば、Windows と Linux にはそれぞれ独自のインターフェイスがあり、コードの移植性が低くなっていました。C++11 の最も重要な機能はスレッドのサポートです。これにより、C++ は並列プログラミングでサードパーティのライブラリに依存する必要がなくなり、アトミック操作にアトミック クラスの概念も導入されます。標準ライブラリのスレッドを使用するには、<thread> ヘッダー ファイルをインクルードする必要があります。
関数名 関数 糸() スレッド関数を関連付けずにスレッド オブジェクトを構築します。つまり、スレッドは開始されません。 スレッド(fn, args1, args2, …) スレッド オブジェクトを構築し、スレッド関数 fn、args1、args2 などをスレッド関数のパラメータとして関連付けます。 get_id() スレッドIDを取得する jionable() スレッドがまだ実行中であるかどうかに関係なく、joinable は実行中のスレッドを表します。 ジオン() この関数が呼び出された後、スレッドはブロックされ、スレッドが終了すると、メインスレッドは実行を継続します。 デタッチ() スレッド オブジェクトの作成直後に呼び出され、作成されたスレッドをスレッド オブジェクトから分離するために使用されます。分離されたスレッドはバックグラウンド スレッドになります。作成されたスレッドの「生死」はメイン スレッドとは関係ありません。 知らせ:
スレッドはオペレーティング システムの概念であり、スレッド オブジェクトをスレッドに関連付けて、スレッドの制御やスレッドのステータスの取得に使用できます。
スレッド オブジェクトが作成されるとき、スレッド関数は提供されず、オブジェクトは実際にはどのスレッドにも対応しません。
#include <thread> int main(){ std::thread t1; cout << t1.get_id() << endl; return 0; } //运行结果 //0
get_id() の戻り値の型は id 型です。id 型は実際には std::thread 名前空間にカプセル化されたクラスです。このクラスには次の構造体が含まれています。
// vs下查看 typedef struct { /* thread identifier for Win32 */ void *_Hnd; /* Win32 HANDLE */ unsigned int _Id; } _Thrd_imp_t;
- スレッド オブジェクトが作成され、スレッド関数がスレッドに関連付けられると、スレッドが開始され、メイン スレッドとともに実行されます。
スレッド関数は通常、次の 3 つの方法で提供できます。
関数ポインタ
ラムダ式
関数オブジェクト
#include <thread> #include <windows.h> void ThreadFun(int value){ cout << "thread" << value << endl; } class TF{ public: void operator()(int value){ cout << "thread" << value << endl; } }; int main(){ std::thread t1(ThreadFun, 1); Sleep(1); TF tf; std::thread t2(tf, 2); Sleep(1); std::thread t3([](int value) { cout << "thread" << value << endl; }, 3); t1.join(); t2.join(); t3.join(); cout << "Main thread!" << endl; return 0; } //运行结果 //thread1 //thread2 //thread3 //Main thread!
スレッド クラスはコピー防止であり、コピーの構築と代入は許可されませんが、移動の構築と移動の代入は許可されます。つまり、スレッド オブジェクトに関連付けられたスレッドの状態が他のスレッド オブジェクトに転送されます。スレッドは転送中に意図されたものではありません。
jionable() 関数を使用すると、スレッドが有効かどうかを判断できます。次のいずれかの状況の場合、スレッドは無効です。
引数なしのコンストラクターを使用して構築されたスレッド オブジェクト
スレッド オブジェクトの状態が他のスレッド オブジェクトに転送されました
jion または detach を呼び出すことにより、スレッドが終了しました。
9.2 スレッド関数パラメータ
スレッド関数のパラメータは、値 copy の形式でスレッド スタック領域にコピーされます。そのため、スレッド パラメータが参照型であっても、外部実パラメータは実際に参照しているため、スレッド内で変更された後に変更することはできません。外部引数ではなく、スレッド スタック内のコピーにコピーします。
#include <thread> void ThreadFunc1(int& x){ x += 10; } void ThreadFunc2(int* x){ *x += 100; } int main(){ int a = 10; // 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际 //引用的是线程栈中的拷贝 /*thread t1(ThreadFunc1, a); t1.join(); cout << a << endl;*/ //如果想要通过形参改变外部实参时,必须借助std::ref()函数,否则程序会报错(vs2019) thread t2(ThreadFunc1, std::ref(a)); t2.join(); cout << a << endl; // 地址的拷贝 thread t3(ThreadFunc2, &a); t3.join(); cout << a << endl; return 0; } //运行结果 //20 //120
注: クラス メンバー関数がスレッド パラメーターとして使用される場合、これはスレッド関数パラメーターとして使用される必要があります。
9.4 アトミック操作ライブラリ(アトミック)
マルチスレッドの主な問題は、共有データ (つまり、スレッド セーフ) によって引き起こされる問題です。共有データが読み取り専用の場合は問題ありません。読み取り専用操作はデータに影響を与えず、ましてやデータを変更しないため、すべてのスレッドが同じデータを取得します。ただし、1 つ以上のスレッドが共有データを変更しようとすると、多くの潜在的な問題が発生します。例えば:
int sum = 0; void fun(int size){ for (int i = 0; i < size; ++i) { sum++; } } int main(){ cout << "运行之前的sum=" << sum << endl; //分别让两个线程同时对同一个变量sum++ thread t1(fun,100000); thread t2(fun,100000); t1.join(); t2.join(); cout << "运行之后的sum=" << sum << endl; return 0; } //运行结果:sum运行之后的值<=200000
C++98 の従来のソリューション: 共有された変更されたデータをロックして保護できる
#include <mutex> std::mutex mt;//创建一把锁 //分别让两个线程同时对同一个变量sum++ int sum = 0; void fun(int size){ for (int i = 0; i < size; ++i){ mt.lock(); sum++; mt.unlock(); } } int main(){ cout << "运行之前的sum=" << sum << endl; thread t1(fun, 100000); thread t2(fun, 100000); t1.join(); t2.join(); cout << "运行之后的sum=" << sum << endl; return 0; } //运行结果 //运行之前的sum = 0 //运行之后的sum = 200000
ロックは解決できますが、ロックの欠点の 1 つは、1 つのスレッドが sum++ を処理している限り、他のスレッドがブロックされ、プログラムの動作効率に影響を与えることです。さらに、ロックが適切に制御されていない場合、簡単にロックが解除されてしまう可能性があります。デッドロックを引き起こす。
したがって、C++11 ではアトミック操作が導入されました。いわゆるアトミック操作: 中断できない 1 つまたは一連の操作 C++11 で導入されたアトミック操作タイプにより、スレッド間のデータの同期が非常に効率的になります。
注: 上記のアトミック操作変数を使用する必要がある場合は、ヘッダー ファイルを追加する必要があります。
#include <atomic> atomic_int sum = 0; void fun(int size){ for (int i = 0; i < size; ++i){ sum++;// 原子操作 } } int main(){ cout << "运行之前的sum=" << sum << endl; thread t1(fun, 100000); thread t2(fun, 100000); t1.join(); t2.join(); cout << "运行之后的sum=" << sum << endl; return 0; } //运行结果 //运行之前的sum = 0 //运行之后的sum = 200000
C++11 では、プログラマはアトミック型変数をロックおよびロック解除する必要がなく、スレッドはアトミック型変数に相互に排他的にアクセスできます。より一般的には、プログラマはアトミック クラス テンプレートを使用して、必要なアトミック タイプを定義できます。
atomic<T> t; // 声明一个类型为T的原子类型变量t
注: 通常、アトミック タイプは「リソース」データに属し、複数のスレッドは単一のアトミック タイプのコピーにのみアクセスできます。したがって、C++11 では、アトミック タイプはテンプレート パラメータからのみ構築でき、アトミック タイプは構築できません。事故を防ぐため、標準ライブラリでは、アトミック テンプレート クラスのコピー コンストラクション、ムーブ コンストラクション、および代入演算子のオーバーロードがデフォルトで削除されています。
#include <atomic> int main() { atomic<int> a1(0); //atomic<int> a2(a1); // 编译失败,尝试引用已删除的函数 atomic<int> a2(0); //a2 = a1; // 编译失败,尝试引用已删除的函数 return 0; }
9.5 lock_guard と unique_lock
マルチスレッド環境では、特定の変数のセキュリティを確保したい場合、その変数を対応するアトミック タイプに設定するだけで済みます。これは効率的であり、デッドロックの問題が発生しにくくなります。ただし、場合によっては、コードのセキュリティを確保する必要があるため、ロックを通じてのみ制御できるようになります。
例: あるスレッドは変数数値に 1 を 100 回加算し、別のスレッドはそれを 100 回減分します。1 を加算または 1 減算する各操作の後に、number の結果が出力されます。要件は、number の最終値が 0 であることです。 。#include <thread> #include <mutex> int number = 0; mutex g_lock; int ThreadProc1(){ for (int i = 0; i < 100; i++){ g_lock.lock(); ++number; cout << "thread 1 :" << number << endl; g_lock.unlock(); } return 0; } int ThreadProc2(){ for (int i = 0; i < 100; i++){ g_lock.lock(); --number; cout << "thread 2 :" << number << endl; g_lock.unlock(); } return 0; } int main(){ thread t1(ThreadProc1); thread t2(ThreadProc2); t1.join(); t2.join(); cout << "number:" << number << endl; return 0; }
上記のコードの欠点:ロック制御が適切でない場合、デッドロックが発生する可能性があります。最も一般的なものは、ロックの途中でコードが返されるか、ロックの範囲内で例外がスローされることです。したがって、C++11 は RAII を使用してロック、つまり lock_guard と unique_lock をカプセル化します。
int ThreadProc1() { for (int i = 0; i < 100; i++) { //lock_guard<mutex> lock(g_lock); unique_lock<mutex> lock(g_lock); ++number; cout << "thread 1 :" << number << endl; } return 0; } int ThreadProc2() { for (int i = 0; i < 100; i++) { //lock_guard<mutex> lock(g_lock); unique_lock<mutex> lock(g_lock); --number; cout << "thread 2 :" << number << endl; } return 0; }
9.5.1 ミューテックスの種類
9.5.1 ミューテックスの種類
- std::mutex は
、C++11 で提供される最も基本的なミューテックスです。このクラスのオブジェクトはコピーまたは移動できません。mutex の最も一般的に使用される 3 つの関数
:
関数名 関数関数 ロック() lock: ミューテックスをロックします ロック解除() ロック解除: ミューテックスの所有権を解放します。 try_lock() ミューテックスをロックしてみてください。ミューテックスが別のスレッドによって占有されている場合、現在のスレッドはブロックされません。 スレッド関数が lock() を呼び出すと、次の 3 つの状況が発生する可能性があることに注意してください。
ミューテックスが現在ロックされていない場合、呼び出しスレッドはミューテックスをロックし、unlock が呼び出されるまでロックを保持します。
現在のミューテックスが他のスレッドによってロックされている場合、現在の呼び出しスレッドはブロックされます。
現在のミューテックスが現在の呼び出しスレッドによってロックされている場合、デッドロックが発生します。
スレッド関数が try_lock() を呼び出すと、次の 3 つの状況が発生する可能性があります。
現在のミューテックスが他のスレッドによって占有されていない場合、スレッドはロック解除を呼び出してミューテックスを解放するまで、ミューテックスをロックします。
現在のミューテックスが別のスレッドによってロックされている場合、現在の呼び出しスレッドは false を返し、ブロックされません。
std::recursive_mutex (再帰ミューテックス)
これにより、同じスレッドがミューテックスを複数回ロック (つまり、再帰的にロック) して、ミューテックス オブジェクトの複数の層の所有権を取得できるようになります。ミューテックスを解放するときは、ロック階層と同じ回数、unlock() を呼び出す必要があります。 Depthですが、 std::recursive_mutex の特性は std::mutex とほぼ同じです。
std::timed_mutex
std::mutex よりも 2 つのメンバー関数、try_lock_for() と try_lock_until() があります。
try_lock_for()
は時間範囲を受け入れます。これは、この期間内にロックを取得しない場合、スレッドがブロックされることを意味します (std::mutex の try_lock() とは異なり、ロックが取得されない場合、try_lock は直接 false を返します)呼び出されたとき) の場合、この期間中に他のスレッドがロックを解放すると、そのスレッドはミューテックスのロックを取得できますが、タイムアウトした場合 (つまり、指定された時間内にロックが取得されなかった場合)、 false が返されます。try_lock_until()
はパラメータとして時点を受け取ります。指定された時点が到着する前にスレッドがロックを取得しない場合、スレッドはブロックされます。この期間中に他のスレッドがロックを解放すると、スレッドはミューテックスのロックを取得できます。タイムアウトの場合 (つまり、指定された時間内にロックが取得されない場合)、 false が返されます。std::recursive_timed_mutex
9.5.2 ロックガード
std::lock_gurad は、C++11 で定義されたテンプレート クラスです。次のように定義されます。
template<class _Mutex> class lock_guard { public: // 在构造lock_gard时,_Mtx还没有被上锁 explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { _MyMutex.lock(); } // 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁 lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { } ~lock_guard() _NOEXCEPT { _MyMutex.unlock(); } lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: _Mutex& _MyMutex; };
上記のコードからわかるように、lock_guard クラス テンプレートは、主に RAII を通じて管理するミューテックスをカプセル化します。ロックが必要な場合は、上で紹介したミューテックスを使用して lock_guard をインスタンス化するだけです。コンストラクターを呼び出して正常にロックした後、lock_guard はオブジェクトはスコープ外に出る前に破棄され、デストラクターが呼び出されてオブジェクトのロックを自動的に解除するため、デッドロックの問題を効果的に回避できます。
lock_guard の欠点は、単純すぎてユーザーが lock を制御できないことです。そのため、C++11 では unique_lock が提供されています。
9.5.3 unique_lock
lock_gard と同様に、unique_lock クラス テンプレートも RAII を使用してロックをカプセル化し、排他的所有権方式でミューテックス オブジェクトのロックおよびロック解除操作を管理します。つまり、オブジェクト間のコピーは発生しません。割り当てを構築するとき (または移動 (移動) するとき)、 unique_lock オブジェクトはパラメータとして Mutex オブジェクトを渡す必要があり、新しく作成された unique_lock オブジェクトは、受信した Mutex オブジェクトのロックおよびロック解除の操作を担当します。上記の種類のミューテックスを使用して unique_lock オブジェクトをインスタンス化すると、コンストラクターが自動的に呼び出されてロックされ、unique_lock オブジェクトが破棄されると、デストラクターが自動的に呼び出されてロックが解除されます。これにより、デッドロックの問題を簡単に防ぐことができます。
lock_guard とは異なり、unique_lock はより柔軟で、より多くのメンバー関数を提供します。
ロック/ロック解除操作: lock、try_lock、try_lock_for、try_lock_until、unlock
変更操作:割り当ての移動、交換(swap:別の unique_lock オブジェクトが管理するミューテックスの所有権を交換)、解放(release:管理しているミューテックス オブジェクトへのポインタを返し、所有権を解放)
属性の取得: owns_lock (現在のオブジェクトがロックされているかどうかを返す)、operator bool() (owns_lock() と同じ関数)、mutex (現在の unique_lock によって管理されているミューテックスのポインタを返す)。
9.6 条件変数
このセクションでは主に、condition_variable の使用方法を説明します。
例: 2 つのスレッドを交互に出力することをサポートします。1 つは奇数を出力し、もう 1 つは偶数を出力します。
#include <iostream> #include <thread> #include <condition_variable> using namespace std; int main(){ mutex mtx; std::condition_variable con;//条件变量 int i = 1; int flag = true; //打印奇数 thread t1([&] { while (i < 100){ unique_lock<mutex> lock(mtx); while (!flag)con.wait(lock);//这里必须是while,不能用if cout <<"t1----" <<this_thread::get_id()<<"-----" << i++ << endl; flag = false; con.notify_one();//通知另一个线程 } }); //打印偶数 thread t2([&] { while (i<=100){ unique_lock<mutex> lock(mtx); while(flag)con.wait(lock); cout << "t2----" << this_thread::get_id() << "-----" << i++ << endl; flag = true; con.notify_one();//通知另一个线程 } }); t1.join(); t2.join(); return 0; }