序文
以前に、C++11 リストの初期化、新しいクラス関数、右辺値参照などの新機能を紹介しましたが、この記事では引き続き、変数パラメーター テンプレートやラムダ式などの新しい構文を紹介します。
1. 可変パラメータテンプレート
C++11 より前では、共通の固定数のテンプレート パラメーターがありましたが、可変パラメーターから始める方法がわかりませんでした。C++11 以降、可変テンプレート パラメーターが導入されました。仮パラメーターは 0 個以上の異なるパラメーターを受け取ることができますまたは同じタイプのパラメータ。以下は特定のコードです。
template<class ...Args>
void showList(Args... args)
{
// ...
}
このうち、Args は変数パラメータの型名であり、別の名前で命名することもできます。args をパラメータ パッケージと呼び、別の名前で命名することもできます。このパラメータ パッケージは 0 個以上のパラメータを受け取ることができます。このパラメータ パッケージの分析には、次の 2 つの方法が使用できます。
1. 2 つの開梱方法
// 方法一:递归取参
void showList()
{
cout << endl;
}
template<class T, class ...Args>
void showList(const T& t, Args... args)
{
cout << t << " ";
showList(args...);
}
void test13()
{
showList();
showList(1);
showList(1, 'x');
showList(1, 1.11, 'y');
}
// 方式二:利用数组自动推导元素个数
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{
// 逗号表达式
int arr[] = { 0, (PrintArg(args), 0)...};
cout << endl;
}
void test13()
{
ShowList();
ShowList(1);
ShowList(1, 'x');
ShowList(1, 1.11, 'y');
}
2. シリーズインターフェイスを配置
emplace シリーズのインターフェイスはすべてプラグイン インターフェイスであり、パラメータの転送にパラメータ パッケージを使用します。STL のほとんどのコンテナはこのプラグイン インターフェイスを提供します。たとえば、次のとおりです。
では、emplace 挿入インターフェイスと通常の挿入インターフェイスの違いは何でしょうか? ここでは例としてベクトルを使用します。
void test14()
{
list<MySpace::string> l1;
MySpace::string str1("hello");
// 无区别
l1.push_back(str1); // 深拷贝
l1.emplace_back(str1); // 深拷贝
cout << endl << endl;
// 无区别
l1.push_back(move(str1)); // 移动构造
l1.emplace_back(move(str1)); // 移动构造
cout << endl << endl;
// 有区别
l1.push_back("11111"); // 拷贝构造 + 移动构造
l1.emplace_back("11111"); // 直接构造
}
2.ラムダ
1. 初期ラムダ
私たちはよく次のようなコードを書きます。
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct CmpByPriceLess
{
bool operator()(const Goods& x, const Goods& y)
{
return x._price < y._price;
}
};
struct CmpByPriceGreater
{
bool operator()(const Goods& x, const Goods& y)
{
return x._price > y._price;
}
};
void test1()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
// 价格升序
sort(v.begin(), v.end(), CmpByPriceLess());
// 价格降序
sort(v.begin(), v.end(), CmpByPriceGreater());
}
sort を使用する場合、受信ファンクターを使用する必要がありますが、このファンクターにどのような名前を渡すかで悩んでいませんか? C++11 では、ラムダである新しいメソッドが導入されています。
2. ラムダの使用
ラムダの具体的な形式は次のとおりです。
[キャプチャリスト](パラメータ) 可変 -> 戻り値の型 { ステートメント };
上記を読んでもまだ少し混乱しているかもしれませんが、大まかに使用してみましょう。
void test1()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
// 价格升序
//sort(v.begin(), v.end(), CmpByPriceLess());
// 价格降序
//sort(v.begin(), v.end(), CmpByPriceGreater());
// 价格升序
sort(v.begin(), v.end(), [](const Goods& x, const Goods& y)mutable->bool
{
return x._price < x._price;
});
// 价格降序
sort(v.begin(), v.end(), [](const Goods& x, const Goods& y)mutable->bool
{
return x._price > x._price;
});
}
まず、角括弧内がキャプチャ リストであり、ラムダ関数で使用するためにコンテキスト内の変数をキャプチャできることを紹介します。いくつかのキャプチャ メソッドがあります。
[var]: 値のキャプチャ; 現時点では値を渡しているだけであり、ラムダ関数内の値を変更してもラムダの外でキャプチャされた値には影響しません; デフォルトでは、キャプチャする値には const 属性があり、マッスルを追加する鍵は可変であることです。 Word は var を変更可能にできます (ラムダの外側でキャプチャされた値を変更することなく)。
[ = ]: すべての値が渡され、上記のすべての変数がキャプチャされ、値がラムダ関数に渡されます。
void test2()
{
int a = 3;
int b = 5;
// 值捕捉
auto add = [a, b]()mutable->int
{
a++;
return a + b;
};
// 全部值捕捉(若无参数,括号可省略;一般情况下,返回值也可以省略)
// 如果不期望修改值传递进来的值,mutable也可以省略;
auto sub = [=] { return a - b; };
cout << add() << endl;
cout << sub() << endl;
cout << a << endl;
}
[ var& ]: 参照渡し、参照によってラムダ関数に値を渡します。ラムダ関数内の値を変更すると、参照によって渡されるため、値自体にも影響します。
[ & ]: 上記のすべての変数を参照によってキャプチャします。
void test3()
{
int x = 3;
int y = 4;
// 若添加了mutable则圆括号不可省略
auto f1 = [&x, y]()mutable {x = 1; y = 2; };
f1();
cout << x << " " << y << endl;
// 将上文所有变量以引用的方式进行捕捉
auto f2 = [&] {x = 10; y = 20; };
cout << x << " " << y << endl;
}
[ this ]: 現在のクラスの this ポインタをキャプチャします。実際、前の = もクラス内の this ポインタをキャプチャしますが、これは値によって渡され、& は参照によって渡されます。
class A
{
public:
void func()
{
// 捕获当前类的this指针
auto f1 = [this] {cout << _a << endl; };
}
private:
int _a = 0;
};
もう一度括弧を見てみましょう. 実際、括弧は関数の括弧と似ており、パラメータを受け取るために使用されます; mutable は以前に導入されました. 通常の値の転送では、通常 const 修飾が追加されます. mutable を追加した後、値の転送は行われませんconst で変更します。矢印の後ろは戻り値であり、通常は必要ありません。中括弧は関数の本体です。
3. ラムダの基本原理
ラムダの使用法については前に説明しましたが、ラムダの基礎となる原理は何でしょうか? その前に、同様のラムダは相互に値を割り当てることができますか?という質問に答えます。次のコード;
// 是否正确?
void test3()
{
auto f1 = [](int x, int y) {return x + y; };
//auto f2 = [](int x, int y) {return x + y; };
auto f2 = [](int x, int y) {return x - y; };
f1 = f2;
}
実際、似ていてもまったく同じでも、値を割り当てることはできません。これはなぜでしょうか。全く同じに見えませんか?そして、ラムダ関数の性質は何でしょうか? これらの問題を説明するために、次のコードを使用します。
struct Fun
{
int operator()(int x, int y)
{
return x + y;
}
};
void test4()
{
auto f1 = [](int x, int y) { return x + y; };
Fun f2;
// 调用lambda函数
f1(2, 5);
// 调用仿函数
f2(2, 5);
}
コードがアセンブリに変換された後の結果が表示されます。
アセンブリに変換された後の 2 つのコードの結果はほぼ同じであることがわかりました。はい、ラムダは実際にはファンクターですが、コンパイラが自動的に型を生成します。クラス名は lambda_ + UUID; UUID です。は一意の識別子であるため、同じラムダ関数に見えますが、実際にはクラス名が異なるファンクターです。型が異なるため、どうやって相互に値を代入すればよいでしょうか? 先ほどの問題は簡単に解決されませんか?
3. 包装
1. 初期機能
私たちのラッパーはアダプターとも呼ばれ、主に呼び出し可能なオブジェクトをラップします。C++ の呼び出し可能なオブジェクトとは何ですか? C 言語で学んだ関数ポインタ、前に学んだファンクター、学んだばかりのラムダ関数があり、次のコードに示すように、これらを同じ方法でカプセル化できます。
struct ADD
{
int operator()(int x, int y)
{
return x + y;
}
};
int add(int x, int y)
{
return x + y;
}
void test5()
{
// 包装器封装仿函数
function<int(int, int)> f1 = ADD();
// 包装器封装函数指针
int(*pf)(int, int) = add;
function<int(int, int)> f1 = pf;
// 包装器封装lambda函数
function<int(int, int)> f1 = [](int x, int y) { return x + y; };
}
以下に示すように、ラッパーは上記の通常関数の一部をカプセル化することができ、クラス内のメンバー関数をラップすることもできます。
class A
{
public:
// 静态成员函数
static int func1(int x, int y)
{
return x + y;
}
// 普通成员函数
int func2(int x, int y)
{
return x + y;
}
private:
};
void test6()
{
// 封装静态成员函数(类名前可加&也可不加)
//function<int(int, int)> f1 = A::func1;
function<int(int, int)> f1 = &A::func1;
// 封装普通成员函数(类型前必须加&,语法规定,且参数列表中声明类名)
function<int(A, int, int)> f2 = &A::func2;
}
2. ラッパーの利用シナリオ
ラッパーは主に、関数を均一に処理する必要があるいくつかのシナリオで使用されます。たとえば、いくつかの戻り値と同じ仮パラメータを持つ呼び出し可能なオブジェクトを格納するベクトルがあります。しかし、これらの呼び出し可能なオブジェクトはファンクターまたは関数ポインターである可能性があります。ラムダ関数である場合もありますが、この場合、それらは同じクラスではなく、ファンクターを介して 1 つのクラスにカプセル化することしかできません。
struct functor
{
int operator()(int x, int y)
{
return x - y;
}
};
int mul(int x, int y)
{
return x * y;
}
void test7()
{
vector<function<int(int x, int y)>> vfs;
// lambda函数
vfs.push_back([](int x, int y)-> int {return x + y; });
// 仿函数
functor ft;
vfs.push_back(ft);
// 函数指针
int(*ptr)(int, int) = mul;
vfs.push_back(ptr);
}
3、バインド
Bind はヘッダー ファイル関数で定義されています。関数テンプレートのアダプターに似ており、上記の関数はクラス テンプレートのアダプターです。バインドには主に 2 つの機能があります。1 つはパラメーターの順序を調整すること、もう 1 つはパラメーターの数を調整することです.以下一つずつ進めていきましょう。
// 调整参数顺序
void Func1(int x, int y)
{
cout << x << " " << y << endl;
}
void test8()
{
// 调整参数顺序
Func1(10, 20);
// placeholder为命名空间,_1,_2....._n都为调整的参数顺序
auto f1 = bind(Func1, placeholders::_2, placeholders::_1);
// 写法二
function<void(int, int)> f2 = bind(Func1, placeholders::_2, placeholders::_1);
f1(10, 20);
}
// 调整参数个数
class Cal
{
public:
Cal(double rate = 2.5)
:_rate(rate)
{}
double cal(int x, int y)
{
return (x + y) * _rate;
}
private:
double _rate;
};
void test9()
{
int x = 3;
int y = 6;
Cal c1;
cout << c1.cal(x, y) << endl;
// 调整参数个数
auto func2 = bind(&Cal::cal, c1, placeholders::_1, placeholders::_2);
cout << func2(x, y) << endl;
}
実際、パラメータの数を調整するには、バインドに表示されるパラメータを渡します。
4. スレッドライブラリ
C++11 以降、オブジェクト指向のスレッド操作メソッドが導入されました。以前と比較して、オブジェクト指向の利点があるだけでなく、クロスプラットフォームの問題も解決されました。Linux でのスレッド操作インターフェイスは同じです。ウィンドウの下のスレッド操作インターフェイスが異なるため、C++11 より前は、スレッド関連の操作を持つプログラムのクロスプラットフォーム機能があり、条件付きコンパイルを通じて 2 セットの実装ソリューションを実装する必要がありましたが、C+ 以降は+11、スレッド ライブラリを使用することでクロスプラットフォームを実現できます (実際には最下層はまだ条件付きコンパイルですが、他の人によって書かれています)。
1. スレッドライブラリ関連のインターフェース
スレッド ライブラリは、次の関連インターフェイスを提供します。
Linux または Windows でのマルチスレッド開発の経験がある場合は、上記のインターフェイスを一目で使用できるかもしれません。まず、コンストラクターを見てみましょう。
デストラクターについて気にする必要はありません。デストラクターは自動的に呼び出されます。代入オーバーロードには右辺値バージョンのみが含まれており、左辺値バージョンは削除されています。
他のインターフェイスも非常に使いやすく、パラメーターのないパブリック メンバー関数です。具体的な関数は次の図に示すとおりです。
2. スレッド ライブラリに関する詳細
詳細 1: get_id で返されるスレッド ID は Linux のような整数ではなく構造化されたデータであり、詳細は以下のとおりです。
// vs下查看
typedef struct
{
/* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
詳細 2:スレッド作成時の最初のパラメーターは、関数ポインター、ファンクター、またはラムダ関数にすることができます。
void Func1()
{
cout << "thread 1" << endl;
}
struct Func2
{
void operator()()
{
cout << "thread 2" << endl;
}
};
int main()
{
// 函数指针
thread t1(Func1);
// 传仿函数对象
Func2 f2;
thread t2(f2);
// 传lambda函数
thread t3([] { cout << "thread 3" << endl; });
t1.join();
t2.join();
t3.join();
return 0;
}
詳細 3 : スレッド関数のパラメータの受け渡しについて、スレッド関数が参照を渡したい場合は ref 関数を呼び出す必要があり、参照とポインタを渡すときに detach を呼び出してスレッドを分離する場合は、参照に注意してください/ ポインタが指すオブジェクトがスタック上のオブジェクトである場合、境界外アクセスが発生する可能性があります。つまり、オブジェクトが配置されているスレッドが終了するか、オブジェクトのスコープ外に出た場合です。オブジェクトが破棄され、子スレッドによるアクセスが継続されると範囲外アクセスが発生する現象。
3. スレッド関連のインターフェース
thread クラスのメンバー関数に加えて、this_thread 名前空間の次の関数もあります。
4. スレッドの安全性に関する問題
同様に、C++11 でカプセル化されたスレッド ライブラリを使用する場合、スレッド セーフの問題が発生します。次のコードはスレッド セーフ コードです。
static int sum = 0;
void transaction(int num)
{
for (int i = 0; i < num; i++)
{
sum++;
}
}
int main()
{
// 创建线程
//template <class Fn, class... Args>
//explicit thread(Fn && fn, Args&&... args);
// 参数一fn通常是一个函数指针,表示该线程调用的函数
// 参数二是可变模板参数,会自动解析,并传给我们参数函数指针所指向的函数
thread t1(transaction, 100000);
thread t2(transaction, 200000);
// 线程等待(必须进行线程等待,除非子线程执行函数中进行了detach)
t1.join();
t2.join();
cout << sum << endl;
return 0;
}
これは明らかに同じコードですが、違いは非常に大きいです。当初期待していた結果は 300000 でした。しかし、上記のようにさまざまな異なる結果があり、通常、それに対処するためにロックを使用します。C++11 ではロックも提供されます。 . カプセル化; 詳細は次のとおりです。
5. ロックの分類
C++11 では具体的に次の 4 つのロックが提供されていますが、ここでは主にミューテックス ロックと通常のミューテックス ロックについて説明します。
ミューテックス ロックには主に次のインターフェイスがあります。
lock はロック (呼び出しをブロックする) を意味し、unlock はロックを解除することを意味します。try_lock はロックを適用しようとすることを意味し、適用されない場合は false を返します。これは非ブロック呼び出しです。これらのインターフェイスを通じて、上記のコードをスレッド化できます。 -安全;
static int sum = 0;
mutex mx; // 创建锁变量
void transaction(int num)
{
// 上锁
mx.lock();
for (int i = 0; i < num; i++)
{
sum++;
}
mx.unlock(); // 解锁
}
int main()
{
thread t1(transaction, 100000);
thread t2(transaction, 200000);
t1.join();
t2.join();
cout << sum << endl;
return 0;
}
6、ロックガード
Lockguard は、RAII メカニズムを使用してロックをカプセル化するミューテックスです。このメカニズムを通じて、特定のローカル スコープでロックを有効にすることができます。実際の原理は、クラスのコンストラクターとデストラクターを使用して実装されます。コンストラクターが定義されているためです。オブジェクト内で呼び出されると、スコープ外に出たときにデストラクタが呼び出されます。コンストラクタ関数 lock でロックを適用し、デストラクタ関数 lock でロックを解除できます。以下は私がシミュレートして実装したロックガードです。
#pragma once
#include <mutex>
template<class Lock>
class Lockguard
{
public:
Lockguard(std::Lock& mt)
:_mt(mt)
{
_mt.lock();
}
~Lockguard()
{
_mt.unlock();
}
private:
std::Lock& _mt;
};
実際、ライブラリにはそのようなクラスがあり、それぞれ lock_gurad と unique_lock と呼ばれます。lock_guard は上で実装したものとほぼ同じです。unique_lock はロックおよびロック解除インターフェイスをいくつか追加しただけです。
次に、lock_guard を使用して、前に書いたコードを再度アップグレードします。
static int sum = 0;
mutex mx; // 创建锁变量
void transaction(int num)
{
// 出作用域自动销毁
lock_guard<mutex> lg(mx);
for (int i = 0; i < num; i++)
{
sum++;
}
}
int main()
{
thread t1(transaction, 100000);
thread t2(transaction, 200000);
t1.join();
t2.join();
cout << sum << endl;
return 0;
}
7. 条件変数
C++ でカプセル化された条件変数は、Linux の条件変数と似ていますが、クロスプラットフォーム機能を提供するためにカプセル化されています。特定のインターフェイスを次の図に示します。
条件変数の使用法を説明するために質問を使用します。
2 つのスレッドを使用し、1 つは奇数を出力し、もう 1 つは偶数を出力し、100 になるまで交互に出力します。