C++11 の新機能の学習 - 「モダン C++ チュートリアル」の読書メモ: 第 3 章 - ラムダ式、関数オブジェクト クラス、および右辺値参照

オンライン閲覧リンク: https://changkun.de/modern-cpp/zh-cn/00-preface/
現在リンク先の「モダン C++ チュートリアル」を読んでおり、学習ノートとして投稿しています。違反した場合は即刻削除します。

第 3 章 言語ランタイムの機能強化

この章では、ラムダ式、関数オブジェクト ラッパー、および右辺値参照を紹介します。そのうち、ラムダ式と右辺値参照は C++11 で導入された重要な機能です。

ラムダ、「関数が必要だけど、わざわざ関数名を付けたくない」という状況に適した、①関数内で定義されたクラス関数として、キャプチャリスト、パラメータリスト、戻り値の型を含めて大まかに理解できます定義内部関数本体の他の部分では、を呼び出すときに必要なパラメータを直接渡し、キャプチャ リストでは値の受け渡し、参照の受け渡し、式による右辺値の受け渡しなどの複数のメソッドを許可し、② Generic Lambda ではさらにパラメータを省略できますlist parameters 型と戻り型の定義により、コンパイラは型推定を個別に実装できます。

関数オブジェクト ラッパー、①空のキャプチャ リスト、関数型、およびその他の呼び出し可能な型std::functionを統合するラムダ式を導入し、これらの関数および関数ポインタの格納、コピー、および呼び出しのためのタイプ セーフなコンテナを提供します; ②使用および処理で「一度に関数のすべてのパラメーターを取得できない」場合、いくつかのパラメーターのバインドとプレースホルダーを実現し、パラメーターが完了してから呼び出しを完了します。std::bindstd::placeholder

右辺値参照, ①T&&宣言を使用して一時変数の宣言期間を延長し、std::move無条件に左辺値を右辺値に変換するメソッドを提供し、右辺値変換および移動操作を使用して元のオブジェクトの作成、コピー、および破棄の操作を置き換えます。従来の C++ を大幅に改善 追加のオーバーヘッドを削減② 右辺値参照の導入後の参照折りたたみルールについては、メソッドを使用して、関数を渡すときに元のパラメーターの型が維持されるようstd::forwardにします。これは と同等です。static_cast<T&&>

3.1 ラムダ式

基本ラムダ

ラムダ関数は実は匿名関数に似た機能を提供しており、「関数が必要だが、わざわざ関数名を付けたくない」という場合に使われます. 基本的な構文は次のとおりです.

[捕获列表](参数列表) mutable(可选/直接省略) 异常属性 -> 返回类型 {
    
    
// 函数体
}

" capture " の変更はわかりにくい. デフォルトでは、Lambda 式の内部関数本体は関数本体の外部の変数を使用できないため、キャプチャ リストは外部パラメーターを渡す役割を果たしますが、パラメーターのみを渡す必要がある場合式は、キャプチャを渡さずに後で呼び出されます。

(1)変数をコピーできる場合、値のキャプチャはパラメータの受け渡しと似ています。キャプチャされた変数は、呼び出されたときではなく、Lambda 式が作成されたときにコピーされます。

int value = 1;
// 定义lambda表达式(值捕获)
auto copy_value = [value] {
    
    
    return value;
};
value = 100;
// 使用lambda表达式
auto stored_value = copy_value();

(2)リファレンス キャプチャ、リファレンス パス パラメータと同様に、リファレンスを保存すると、値が変更されます

int value = 1;
// 定义lambda表达式(引用捕获)
auto copy_value = [&value] {
    
    
    return value;
};
value = 100;
// 使用lambda表达式
auto stored_value = copy_value();

(3) Implicit capture . 最初の 2 つのキャプチャ方法と比較して、ラムダ式もに変数名を記述せ[]、 **[] にキャプチャ タイプを指定し、対応する変数名を関数本体で直接使用できます。コンパイラは、キャプチャ リストをそれ自体で推測します。具体的には、[&]参照によるキャプチャ、[=]値によるキャプチャです。

(4) [式のキャプチャ]ただし、上記のキャプチャ方法は、右辺値ではなく左辺値をキャプチャします。Starting from C++14, the capture member is allowed to be initialized with any expression. たとえば、次のコードは、important"=" 値でキャプチャできない排他的なポインターです。右辺値: 式の初期化。

#include <iostream>
#include <memory>  // std::make_unique
#include <utility> // std::move

void lambda_expression_capture() {
    
    
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
    
    
        return x+y+v1+(*v2);
    };
    // lambda表达式的使用,这里的3,4对应了x,y
    std::cout << add(3,4) << std::endl;
}

アプリケーション:ソート操作にラムダ式を使用し、いくつかの複雑な関数の可能性を作成します

std::vector<int> v = {
    
    3, 1, 5, 4, 2};
sort(v.begin(), v.end(), [](int a, int b){
    
    
    return b < a;
});

汎用ラムダ

C++14 以降、Lambda 関数の仮パラメーターは、autoキーワードを使用してジェネリック型を生成できます。

auto add = [](auto x, auto y) {
    
    
    return x+y;
};
add(1, 2);
add(1.1, 2.2);

3.2 関数オブジェクトラッパー

std::function 呼び出し可能な型

ラムダ式の本質は、関数オブジェクト型と同様のクラス型 (クロージャ型と呼ばれる) のオブジェクト (クロージャ オブジェクトと呼ばれる) です. ラムダ式のキャプチャ リストが空の場合、それはまた、として呼び出すことができます次のように関数ポインタを渡します。

#include <iostream>

using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
void functional(foo f) {
    
     // 参数列表中定义的函数类型 foo 被视为退化后的函数指针类型 foo*
    f(1); // 通过函数指针调用函数
}

int main() {
    
    
    auto f = [](int value) {
    
    
        std::cout << value << std::endl;
    };
    functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
    f(1); // lambda 表达式调用
    return 0;
}

ご覧のとおり、呼び出しには 2 つの異なる方法があります: ① 呼び出す関数型として渡す; ② ラムダ式を直接呼び出す。C++ では、std::function呼び出すことができるこれらのオブジェクトの型を統一する型が導入されており、これらを総称して呼び出し可能型 と呼びます。

std::functionのインスタンスは、呼び出すことができる任意のターゲットエンティティを保存、コピー、および呼び出すことができます. 関数ポインタの呼び出しと比較して、それはタイプセーフなパッケージです. 実際、これはfunctions のコンテナーを提供するため、関数と関数ポインターをオブジェクトとして扱うのがより便利になります。

#include <functional>
#include <iostream>

int foo(int para) {
    
    
    return para;
}

int main() {
    
    
    // std::function 包装了一个返回值为 int, 参数为 int 的函数
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int {
    
    
        return 1+value+important;
    };
    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
}

std::bind と std::placeholder

std::bind関数呼び出しをバインドするために使用されるパラメーターは、「関数を呼び出すすべてのパラメーターを一度に取得できない」状況に適用ます。プレースホルダーを指定し、パラメーターが完了したら呼び出しを完了します。好き:std::placeholder

#include <functional>
int foo(int a, int b, int c) {
    
    
    ;
}
int main() {
    
    
    // 将参数1,2绑定到函数 foo 上,
    // 但使用 std::placeholders::_1 来对第一个参数进行占位
    auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
    // 这时调用 bindFoo 时,只需要提供第一个参数即可
    bindFoo(1);
}

3.3 右辺値参照

右辺値参照の導入により、多くの歴史的な問題が解決され、 などの追加のオーバーヘッドが排除されstd::vectorstd::string関数オブジェクト クラスがstd::function可能になります。

C++11 で導入された右辺値参照は、&&で示され、変更できます。

int && a = 10;
a = 100;

概念の理解: 左辺値、右辺値、右辺値の prvalue、瀕死の値

  • 左辺値: lvalue, left value、代入記号の左側の値、より正確には式 の後に持続するオブジェクト
  • Rvalue : rvalue, right value、右側の値、式の終了後に存在しなくなる一時的なオブジェクト。(従来の C++ の prvalue と同じ概念です)
  • 純粋な右辺値: prvaluepure rvalue、純粋なリテラル10( 、などtrue) または評価結果は、リテラル/匿名の一時オブジェクト( など)、非参照によって返される一時変数、演算式によって生成される一時変数、プリミティブ リテラル、ラムダ式と同等です。 . (文字列リテラルは座る値であり、型は配列であることに注意してください)1+2const char
  • Xvalue : xvalue, expiring value, 右辺値参照を導入するために C++11 によって提案された概念、つまり、破棄されようとしているが移動できる値一時的な値を認識して同時に移動できるような動作を定義します。次のコード片の分析を見ることができます。
std::vector<int> foo() {
    
    
    std::vector<int> temp = {
    
    1, 2, 3, 4};
    return temp;
}

std::vector<int> v = foo();

上記のコードで

  • 従来の C++ では、関数fooの戻り値は、オブジェクトtemp内部的に作成された後にオブジェクトに割り当てられます.オブジェクトを取得したv後、オブジェクトv全体がコピーされ破棄されます. 追加のオーバーヘッド.temptemptemp
  • C++11 の後、コンパイラは、ここで左辺値に対して暗黙的な右辺値変換tempを実行しローカルに返された値を移動する作業を行いました。これは、後述の移動セマンティクスです。vfoo

ここでも、定数非定数の概念を区別する必要があります. 定数を定義するとき、それはconst変更され、したがって 4 種類の (非) 定数の左右の値が形成されます. それらの可能な参照と使用シナリオは次のとおりです:

値型 →
参照型 ↓
非定数左辺値 const 左辺値 非定数右辺値 const 右辺値 使用シーン
非 const 左辺値参照 N N N なし
const 左辺値リファレンス コピーコンストラクターを構築するためにクラスで一般的に使用されます
非 const 右辺値参照 N N N モバイル セマンティクス、完全転送
const 右辺値リファレンス N N 実用的ではない

参考:http://c.biancheng.net/view/7829.html

[右辺値参照と左辺値参照]

右辺値参照T &&の宣言により、一時値の有効期間を延長できます。C++11std::moveでは、左辺値パラメーターを無条件に右辺値に変換するこのメソッドが提供されています。

非 const は非左辺値にバインドできません; const は非左辺値にバインドできます。

セマンティクスを移動する

従来の C++では、リソースを移動する場合、呼び出し側でコピーして破棄するか、移動するオブジェクトのインターフェイスを自分で実装する必要があり、古い家にあるものをすべて破棄または破棄するのは理にかなっていません。ここでの従来の C++ の問題は、移動とコピーの概念の区別がなく、大量の不要なデータ コピーが発生し、時間とスペースが浪費され、std::move右辺値参照の出現がこの問題を解決するだけであるということです (#include <utility>ヘッダー ファイル) または②関数の戻り値の実装。

std::moveそれが実際に行うことは、左辺値を右辺値に変換することです。

移動セマンティクスを使用する例として、次の 2 つのコードを見てください (それぞれ①と②による)。

#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string

int main() {
    
    

    std::string str = "Hello world.";
    std::vector<std::string> v;

    // 将使用 push_back(const T&), 即产生拷贝行为
    v.push_back(str);
    // 将输出 "str: Hello world."
    std::cout << "str: " << str << std::endl;

    // 将使用 push_back(const T&&), 不会出现拷贝行为
    // 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
    // 这步操作后, str 中的值会变为空
    v.push_back(std::move(str));
    // 将输出 "str: "
    std::cout << "str: " << str << std::endl;

    return 0;
}
#include <iostream>
class A {
    
    
public:
    int *pointer;
    A():pointer(new int(1)) {
    
    
        std::cout << "构造" << pointer << std::endl;
    }
    A(A& a):pointer(new int(*a.pointer)) {
    
    
        std::cout << "拷贝" << pointer << std::endl;
    } // 无意义的对象拷贝
    A(A&& a):pointer(a.pointer) {
    
      // 如果没有这一函数,下面的obj构造将自动调用上面的“拷贝”方式的构造函数
        a.pointer = nullptr;
        std::cout << "移动" << pointer << std::endl;
    }
    ~A(){
    
    
        std::cout << "析构" << pointer << std::endl;
        delete pointer;
    }
};
// 防止编译器优化
A return_rvalue(bool test) {
    
    
    A a,b;
    if(test) return a; // 等价于 static_cast<A&&>(a);
    else return b;     // 等价于 static_cast<A&&>(b);
}
int main() {
    
    
    A obj = return_rvalue(false);
    std::cout << "obj:" << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
}

走行結果と解説

构造0x2425c20  // a被创建出来
构造0x2425c40  // b被创建出来
移动0x2425c40  // return b时,b作为右值引用传递到obj的构造函数中,指针被移动给obj
析构0  // return_rvalue函数运行结束,对涉及的临时变量进行析构,移动完成后b本身变为了nullptr
析构0x2425c20  // a的指针地址
obj:
0x2425c40
1
析构0x2425c40  // 程序结束,析构obj

完璧な転送

従来の C++ では、引き続き参照型を参照することはできませんが、C++ では右辺値参照の出現によりこの要件が緩和され、いわゆる「参照折りたたみ規則」が導入され、参照 (左/右) が参照できるようになります。従う ルールは次のとおりです。

関数パラメータ型(パラメータ定義型) 実パラメータ型(着信型) 推定パラメーターの型 (関数内で使用される型)
T& 左引用 T&
T& 右引用 T&
T&& 左引用 T&
T&& 右引用 T&&
void reference(int& v) {
    
    
    std::cout << "左值" << std::endl;
}
void reference(int&& v) {
    
    
    std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {
    
                                            // 函数形参类型
    std::cout << "普通传参:";
    reference(v); // 始终调用 reference(int&)    // 推导后形参类型
}
int main() {
    
    
    std::cout << "传递右值:" << std::endl;
    pass(1); // 1是右值, 但输出是左值       // 实参参数类型是右引用,但是进入pass后实际调用了 void reference(int& v)

    std::cout << "传递左值:" << std::endl;
    int l = 1;
    pass(l); // l 是左值, 输出左值            // 实参参数类型

    return 0;
}

この参照崩壊ルールが存在するため、渡す過程で右参照が左参照になる可能性があるため、パラメーターを渡すときに元のパラメーターの型を維持するのに役立つ何らかの規則が必要です (左参照は左参照を保持し、右参照は残ります)。右参照)、これは とstd::forward同等ですstatic_cast<T&&>使用方法は次のとおりで、重複コピーが発生せず、関数引数の完全な転送も実現されます。

void reference(int& v) {
    
    
    std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
    
    
    std::cout << "右值引用" << std::endl;
}

reference(std::forward<T>(v)); // 保持v原本的参数类型(左/右引用)
// 等效于
reference(static_cast<T&&>(v));

【ループ文auto&&での】

おすすめ

転載: blog.csdn.net/lj164567487/article/details/126853600