C++11 の新機能 - nullptr、バインド、ラムダ関数、スマート ポインター、左辺値右辺値、移動セマンティクス、および完全転送

nullptr

NULL は一般に C 言語のマクロ定義に由来します。C++ 言語では NULL は整数 0 として定義されますが、これでは整数 0 と区別できなくなるため、C++11 では整数を区別できる新機能 nullptr が導入されました。ポインタの特性を確認しますが、それでも問題があります。ポインタの型を区別できないため、この場合はパラメータの型を明示的に指定する必要があります。

forward_list

一方向にしか移動できません。

  • メモリ使用量が小さい
  • 高い挿入および削除効率
  • ランダムアクセスなし
  • 挿入と削除によりリンク リストの構造が変更されるため、イテレータが無効になる可能性があります
  • 逆方向のトラバーサルはサポートされていません

練る

変数の値は参照によって直接操作でき、bind 関数を使用して関数オブジェクトとパラメーター リストを呼び出し可能なオブジェクトにバインドできます。バインド関数で参照を渡したい場合は、 std::ref() 関数を使用して参照オブジェクトをラップできます。次に例を示します。

#include <functional>
#include <iostream>

void add(int& a, int& b)
{
    
    
    a += b;
}

int main()
{
    
    
    int a = 10;
    int b = 5;

    auto func = std::bind(add, std::ref(a), std::ref(b));

    func();

    std::cout << "a = " << a << std::endl; //a=15
    std::cout << "b = " << b << std::endl; //b=5

    return 0;
}

auto と decltype

auto は初期値を通じて型推論を実行できるため、auto 型には初期値が必要です。
decltype の機能は、オペランドのデータ型を選択して返すことですが、このプロセスでは、コンパイラは式の値を実際に計算せずに、式の型を取得するだけです。

ラムダ

匿名関数はコード内で直接定義でき、ラムダ関数はパラメータとして関数に渡すことができ、アルゴリズムを戻り値として使用することもできます。コンパイラは、クロージャ型と呼ばれる匿名クラスを生成します。
キャプチャ リストは外部変数をキャプチャするために使用され、空にすることも、1 つ以上の変数を含めることもできます。パラメータ リストと戻り値の型 (関数本体が式の場合、戻り値の型は自動的に推測できます。ステートメント ブロックの場合は、戻り値の型を明示的に指定する必要があります) は、関数のパラメータ リストと戻り値の型と同じです。通常の機能。function body は関数本体であり、一連のステートメントを含めることができます。

[capture list] (parameter list) -> return type {
    
     function body }

auto add = [](int a, int b) -> int {
    
    
    return a + b;
};

int sum = add(1, 2); // sum = 3

Lambda 関数は外部変数もキャプチャできます

int x = 10;

auto print = [x](int n) {
    
    
    std::cout << "x = " << x << ", n = " << n << std::endl;
};

print(5); // x = 10, n = 5

関数オブジェクトもサポートされており、比較や並べ替えなどのアルゴリズムの実装に使用できます。

std::vector<int> v{
    
    5, 1, 3, 2, 4};

std::sort(v.begin(), v.end(), [](int a, int b) {
    
    
    return a > b;
});

ラムダ関数はパラメータ リストと戻り値を省略できますが、キャプチャ リストと関数本体を含める必要があります。

スマートポインター

ヒープメモリリークを防ぎます。スマート ポインタは、動的に割り当てられたオブジェクトへのポインタを格納するために使用されるテンプレート クラスであり、動的に割り当てられたオブジェクトを自動的に解放する役割を果たします。RAIIメカニズムに準拠

共有_ptr

動的に割り当てられたオブジェクトを管理するために使用される複数のスマート ポインターは、オブジェクトを共有し、すべてのスマート ポインターがオブジェクトを参照しなくなったときにオブジェクトを自動的に解放できます。参照カウントを使用すると、使用されるたびにカウントが 1 ずつ増加し、破棄されるとカウントが 1 ずつ減分されます。オブジェクトの読み取りはロックする必要があり、内部参照カウントはスレッドセーフです。

shared_ptr には循環参照の問題があります。つまり、2 つのオブジェクトがshared_ptr メンバー変数を使用して相互に参照します。

弱い_ptr

したがって、weak_ptr が導入され、弱参照は一般にshared_ptrを支援するために使用され、通常のポインタと同様にオブジェクトのメモリを管理しませんが、弱参照は管理対象オブジェクトが解放されたかどうかを検出できます。弱参照はカウントではなく参照のみを行います。また、弱参照は指すメモリが必ずしも有効であることを保証しません。
std::weak_ptr は std::shared_ptr によって初期化できます。std::shared_ptr オブジェクトが破棄されても、std::weak_ptr はオブジェクトの参照カウントに影響を与えず、オブジェクトを自動的に解放しません。逆に、オブジェクトを使用する必要がある場合は、std::weak_ptr の lock() メソッドを通じて使用可能な std::shared_ptr オブジェクトを取得できます。オブジェクトが解放されている場合は、空の std::shared_ptr オブジェクトが返されます。 。

なぜ循環参照の問題は解決できるのでしょうか?

  • 参照カウントが増加しないように、循環参照内のshared_ptrポインタをweak_ptrポインタに置き換えることができます。
#include <iostream>
#include <memory>

struct Node;

struct Edge {
    
    
    std::weak_ptr<Node> end;
};

struct Node {
    
    
    std::vector<std::shared_ptr<Edge>> edges;
};

int main() {
    
    
    std::shared_ptr<Node> node1(new Node);
    std::shared_ptr<Node> node2(new Node);

    node1->edges.push_back(std::make_shared<Edge>());
    node1->edges.back()->end = node2;

    // 在使用node2之前,先通过weak_ptr获取node2
    std::shared_ptr<Node> node2_copy = node1->edges.back()->end.lock();
    if (node2_copy) {
    
    
        std::cout << "node2 is alive" << std::endl;
    } else {
    
    
        std::cout << "node2 is dead" << std::endl;
    }

    // 手动销毁node2
    node2.reset();

    // 再次使用node2
    node2_copy = node1->edges.back()->end.lock();
    if (node2_copy) {
    
    
        std::cout << "node2 is alive" << std::endl;
    } else {
    
    
        std::cout << "node2 is dead" << std::endl;
    }

    return 0;
}

make_shared

make_shared は C++11 で導入されたテンプレート関数で、動的に割り当てられたオブジェクトへの std::shared_ptr スマート ポインターを作成し、オブジェクトのメモリ割り当てとコンストラクター呼び出しを組み合わせるために使用できます。これにより、パフォーマンスとメモリ使用量が向上します。効率。make_shared を使用すると、複数のメモリ割り当てを回避し、メモリの断片化を減らし、動的に割り当てられたオブジェクトの構築と破棄の回数を減らすことができるため、プログラムのパフォーマンスが向上します。

shared_ptrクラスを実装する

1.shared_ptr は、すべてのポインターが解放されるまでオブジェクトが削除されないように、同じオブジェクトを指すポインターの数をカウントする必要があります。
2.shared_ptr は null ポインタを正しく処理する必要があります。
3.shared_ptr は、値を正しくコピーして割り当てることができる必要があります。

コードソースAxiu の勉強ノート

template<typename T>
class SharedPtr
{
    
    
public:
	SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1))
	{
    
    }

	SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount){
    
    
		(*_pcount)++;
	}

	SharedPtr<T>& operator=(const SharedPtr& s){
    
    
		if (this != &s)
		{
    
    
			if (--(*(this->_pcount)) == 0)
			{
    
    
				delete this->_ptr;
				delete this->_pcount;
			}
			_ptr = s._ptr;
			_pcount = s._pcount;
			*(_pcount)++;
		}
		return *this;
	}
	T& operator*()
	{
    
    
		return *(this->_ptr);
	}
	T* operator->()
	{
    
    
		return this->_ptr;
	}
	~SharedPtr()
	{
    
    
		--(*(this->_pcount));
		if (*(this->_pcount) == 0)
		{
    
    
			delete _ptr;
			_ptr = NULL;
			delete _pcount;
			_pcount = NULL;
		}
	}
private:
	T* _ptr;
	int* _pcount;//指向引用计数的指针
};

make_shared は C++11 以降の関数テンプレートであり、ヘッダー ファイルは動的メモリにオブジェクトを割り当てて初期化し、このオブジェクトを指すshared_ptr を返すために使用されます。使用方法は次のとおりです。

std::shared_ptr sp = std::make_shared(args…);
make_shared を使用すると効率が向上し、shared_ptr(new T(args…)) の使用を回避できます。

unique_ptr

排他的所有権、unique_ptr の転送では、すべての所有権がソース ポインターからターゲット ポインターに転送され、元のポインターは空になります。Unique_ptr は、通常のコピーおよび代入操作をサポートしません。unique_ptr をコピーすると、コピー終了後に同じリソースを指すことになります。最終的には、同じメモリ ポインタが複数回解放され、プログラムがクラッシュすることになります。

unique_ptr を実装する

unique_ptr はコピーできないため、コピー コンストラクターとコピー代入演算子は無効になります。
次に、移動コンストラクターと移動演算子は、ポインターの所有権をターゲットの unique_ptr に譲渡し、元のポインターは nullptr になります。

template class unique_ptr {
    
     
public: 
	unique_ptr() : ptr(nullptr) {
    
    }
	explicit unique_ptr(T* p) : ptr(p) {
    
    }
	
	unique_ptr(const unique_ptr& other) = delete;
	
	unique_ptr& operator=(const unique_ptr& other) = delete;
	
	unique_ptr(unique_ptr&& other) : ptr(other.ptr) {
    
     other.ptr = nullptr; }
	
	unique_ptr& operator=(unique_ptr&& other) {
    
    
	    if (&other == this) return *this;
	    delete ptr;
	    ptr = other.ptr;
	    other.ptr = nullptr;
	    return *this;
	}

	~unique_ptr() {
    
     if (ptr) delete ptr; }
	
	T& operator*() const {
    
     return *ptr; }
	
	T* operator->() const {
    
     return ptr; }
	//返回对象原始指针
	T* get() const {
    
     return ptr; }

	void reset() {
    
    
	    delete ptr;
	    ptr = nullptr;
	}
	
	void reset(T* p) {
    
    
	    delete ptr;
	    ptr = p;
	}
private: 
	T* ptr;
};

 int main() {
    
     
 	unique_ptr up(new int(42)); 
 	std::cout << up << std::endl; // 输出42 
 	up.reset(new int(17));
    std::cout << up << std::endl; // 输出17 
  }

auto_ptr

通常のポインタでは空間が解放されず、例外スロー時にメモリリークが発生する可能性があるため、「例外スロー時にメモリリークが発生する」という問題を解決するため。Auto_ptr は、オブジェクトが構築されるときにオブジェクトの制御を取得し、オブジェクトが破棄されるときにオブジェクトを解放します。

  • auto_ptr オブジェクトが破棄されると、そのオブジェクトが所有するポインタは削除されますが、これを使用すると、複数の auto_ptr オブジェクトが同じポインタを管理することができなくなります。
  • Auto_ptr は内部で「delete」を使用するため、配列を管理できません。

左辺値と右辺値

  • C++11 では、すべての値は lvalue と rvalue のいずれかに属している必要があり、rvalue は prvalue と xvalue に細分化できます。C++11 では、アドレスを取得でき名前を持つものを左辺値、逆にアドレスを取得できず名前を持たないものを右辺値 (xvalues または prvalues) と呼びます。

  • 左辺値は、等号の左側または右側に現れる変数 (または式) を指しますが、右辺値は等号の右側にのみ現れることができます。

  • 左辺値は永続的、右辺値は一時的、左辺値は永続的な状態を持ち、右辺値はリテラル定数または式の評価中に作成される一時オブジェクト (破棄されようとしているオブジェクト) のいずれかです。

  • prvalue は、オブジェクトに関連付けられていない一時変数およびリテラル値を指します。prvalue は、他の変数が使用されなくなったことを保証するときに、宣言サイクルを盗んで延長することによってメモリ領域の解放を防ぎます。

  • 右辺値は、定数値、関数の戻り値、ラムダ式など、アドレスを取得できないオブジェクトを表します。アドレスは取得できませんが、変更できないわけではなく、右辺値の右辺値参照が定義されている場合は、右辺値を変更できます。

左辺値参照と右辺値参照

  • 通常、右辺値には名前がないため、参照によってのみ見つけることができます。右辺値参照が C++11 で追加されました。右辺値参照が右辺値に関連付けられている場合、右辺値は特定の場所に格納され、右辺値参照は特定の場所を指します。つまり、右辺値は取得できませんが、アドレス、右辺値 参照は、一時オブジェクトの格納場所を表すアドレスを取得できます。
  • 左辺値参照は特定の変数値の別名であり、右辺値参照は匿名変数の別名です。定数左辺値参照は、非定数左辺値、定数左辺値、およびそれを初期化する右辺値を受け入れることができますが、非定数左辺値は、それを初期化するために非定数左辺値のみを受け入れることができます。
  • 通常、rvalue 参照はどの lvalue にもバインドできません。lvalue を rvalue 参照にバインドするには、通常、 std::move() で lvalue を rvalue にキャストする必要があります。
  • 右辺値参照は左辺値と右辺値から独立しています。つまり、右辺値参照型の変数は左辺値または右辺値である可能性があります。

(&) 左辺値参照と (&&) 右辺値参照の深い理解

コードソースAxiu の勉強ノート

#include <bits/stdc++.h>
using namespace std;

template<typename T>
void fun(T&& t)
{
    
    
	cout << t << endl;
}

int getInt()
{
    
    
	return 5;
}

int main() {
    
    
	
	int a = 10;
	int& b = a;  //b是左值引用
	int& c = 10;  //错误,c是左值不能使用右值初始化
	int&& d = 10;  //正确,右值引用用右值初始化
	int&& e = a;  //错误,e是右值引用不能使用左值初始化
	const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
	const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化
	const int&& h = 10; //正确,右值常引用
	const int& aa = h;//正确
	int& i = getInt();  //错误,i是左值引用不能使用临时变量(右值)初始化
	int&& j = getInt();  //正确,函数返回值是右值
	fun(10); //此时fun函数的参数t是右值
	fun(a); //此时fun函数的参数t是左值
	return 0;
}

動く

  • 移動セマンティクス: IO、unique_ptr など、一部のタイプのリソースは一意です。それらはコピーできませんが、リソースの所有権を新しいオブジェクトに渡して移動可能になります。
  • 移動セマンティクスでは、一部のオブジェクトの構築中に、新しいメモリをコピーして適用することなく、既存のリソースを取得できます。このように、データのコピーは深いコピーではなく、浅いコピーになります。コピーの代わりにこの移動を行うと、パフォーマンスが大幅に向上します。たとえば、消滅しそうな一部の右辺値を移動セマンティクスで引き継ぐことができます。

移動の実施

実際、move は左辺値を右辺値に変換する型コンバーターです

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    
    
    return static_case<typename remove_reference<T>::type&&>(t);
}
参照の削除

テンプレートから参照を削除する

通用引用

move の入力パラメーターの型はユニバーサル参照型と呼ばれ、ユニバーサル参照はlvalue と rvalue の両方を受け取ることができます
コード ソースC++ の高度な知識: モバイル コンストラクターとその原理の詳細な分析

#include<iostream>
template<typename T>
void f(T&& param){
    
    
    std::cout << "the value is "<< param << std::endl;
}

int main(int argc, char *argv[]){
    
    

    int a = 123;
    auto && b = 5;   //通用引用,可以接收右值

    int && c = a;    //错误,右值引用,不能接收左值

    auto && d = a;   //通用引用,可以接收左值

    const auto && e = a; //错误,加了const就不再是通用引用了

    func(a);         //通用引用,可以接收左值
    func(10);        //通用引用,可以接收右值
}

上記のコードには 2 種類のユニバーサル参照があり、1 つは auto で、もう 1 つはテンプレートで定義された T&& ですが、実際、auto と T は同等です。

完璧な転送

前方

この機能は、元の値の属性を変更せずに維持することです。つまり、左辺値の場合は処理後も左辺値のままであり、右辺値の場合は処理後も右辺値のままです。C++ の完全転送に関する
コード ソースの説明

#include<iostream>
template<typename T>
void print(T & t){
    
    
    std::cout << "lvalue" << std::endl;
}

template<typename T>
void print(T && t){
    
    
    std::cout << "rvalue" << std::endl;
}

template<typename T>
void testForward(T && v){
    
    
    print(v);
    print(std::forward<T>(v));
    print(std::move(v));
}

int main(int argc, char * argv[])
{
    
    
    testForward(1);

    std::cout << "======================" << std::endl;

    int x = 1;
    testFoward(x);
}

操作の結果は次のとおりです

lvalue
rvalue
rvalue
=========================
lvalue
lvalue
rvalue

入力される 1 は右辺値ですが、関数がパラメータとして渡された後は左辺値になります。2 行目は forward 関数を使用するため、その右辺値属性は変更されません。3 行目の移動により、入力パラメータが強制的に変換されます。左の値へ。
入力される x は左辺値であり、転送後も左辺値であり、移動後には右辺値になります。

前向きの実装原則

template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
    
    
    return static_cast<T&&>(param);
}

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
    
    
    return static_cast<T&&>(param);
}

完全な転送に失敗しました

完全転送とは、ある関数のパラメーター リスト内のパラメーターをそのまま別の関数に転送することを指しますが、完全転送は失敗する可能性があります。

  • 関数テンプレートと非関数テンプレートの引数の型が一致しません
  • テンプレート パラメーターの推論の失敗
    完全転送の失敗の原因となる実際のパラメーターの種類は、中括弧の初期化、値 0 または NULL を持つ null ポインター、宣言された整数の静的 const メンバー変数のみ、テンプレートまたはオーバーロードされた関数名、およびビット フィールドです。
    項目 30. 完全転送の失敗シナリオをよく理解する

おすすめ

転載: blog.csdn.net/qaaaaaaz/article/details/130655648