C++ 発展編 7日目

この章では、C++11で導入されたスマートポインタについて説明します

スマートポインタ

メモリ管理を簡単にしたスマートポインタがC++11から搭載され、標準ライブラリに含まれています
スマートポインタとは、ポインタをラップしたクラスで、-> や * などの演算子をオーバーロードしています
以下のような特徴があります

  • ポインタが指すメモリを自動的に管理してくれるので、プログラマはメモリの解放を気にする必要がない
  • 例外安全(例外が発生したときに適切に処理される)で、スコープから外れるときにデストラクタが呼ばれてメモリを解放する
  • リソースの自動解放は、ポインタの「所有権」をもとに実行される

C++には、 ヘッダーに定義されている3種類のスマートポインタがあります

  1. std::unique_ptr
    一意に所有される動的に割り当てられたリソースを指すスマートポインタです
    所有権をコピーすることはできませんが、譲渡(ムーブ)することはできます
    以下は、unique_ptrを使用しスマートポインタを譲渡したサンプルプログラムで、unique_ptrの特性を検証しています
#include <iostream>
#include <memory>

class Rectangle {
private:
    int length;
    int breadth;
public:
    Rectangle(int l, int b) {
        length = l;
        breadth = b;
    }
    int area() {
        return length * breadth;
    }
};

int main() {
    // スマートポインタを宣言し、new でオブジェクトを作成して渡す
    std::unique_ptr<Rectangle> p1(new Rectangle(10, 5));
    // -> でオブジェクトのメソッドを呼ぶ
    std::cout << "Area: " << p1->area() << std::endl;
    // ムーブで別のスマートポインタに所有権を移す
    std::unique_ptr<Rectangle> p2 = std::move(p1);

    // p1 はもうオブジェクトを指していない
    if (p1 == nullptr) {
        std::cout << "p1 is nullptr" << std::endl;
    }
    // p2 はオブジェクトを指している
    std::cout << "Area: " << p2->area() << std::endl;
    // p2 がスコープから外れると、オブジェクトは削除される
    return 0;
}
実行結果
Area: 50
p1 is nullptr
Area: 50

std::unique_ptr<Rectangle> p1(new Rectangle(10, 5));

この行は、std::unique_ptr を使って動的にメモリを確保し、そのオブジェクトの所有権を p1 に持たせています
具体的には、new Rectangle(10, 5) でヒープメモリ上に Rectangle オブジェクトが作成され、そのオブジェクトの管理を p1 が引き受けます

unique_ptr の特性として、ポインタをコピーすることはできません
これは、「唯一の所有権」を持つという unique_ptr の性質によるものです
他の unique_ptr にオブジェクトの所有権を移したい場合は、std::move 関数を使います
これにより、元の unique_ptr はオブジェクトの所有権を失い、新しい unique_ptr がその所有権を引き継ぎます
以下の行で、所有権の譲渡を行っています

// ムーブで別のスマートポインタに所有権を移す
std::unique_ptr<Rectangle> p2 = std::move(p1);

所有権が移譲されると、元の p1 は何も指さなくなり nullptr になります

// p1 はもうオブジェクトを指していない
if (p1 == nullptr) {
    std::cout << "p1 is nullptr" << std::endl;
}

そして、所有権を受け取った p2 がそのオブジェクトにアクセスできるようになります

// p2 はオブジェクトを指している
std::cout << "Area: " << p2->area() << std::endl;

従来のポインタ操作では、new でメモリを確保したら、必ず delete を実行してメモリを解放する必要がありました
しかし、unique_ptr のようなスマートポインタを使うと、スコープを抜けるタイミングで自動的に delete が呼び出され、メモリが解放されます
これにより、メモリリークのリスクを大幅に減らすことができます

次に、unique_ptrのスマートポインタを関数の引数にした場合のサンプルプログラムを示します
まず、値渡しの場合のサンプルプログラムです

void functionA(std::unique_ptr<Rectangle> ptr){
    std::cout << "in functionA" << std::endl;
}
int main() {
    // スマートポインタを宣言し、new でオブジェクトを作成して渡す
    std::unique_ptr<Rectangle> p1(new Rectangle(10, 5));
    // -> でオブジェクトのメソッドを呼ぶ
    std::cout << "Area: " << p1->area() << std::endl;

    // 値渡しでfunctionAを呼び出す
    functionA(std::move(p1));

    // p1 はもうオブジェクトを指していない
    if (p1 == nullptr) {
        std::cout << "p1 is nullptr" << std::endl;
    }
    return 0;
}
実行結果
Area: 50
in functionA
p1 is nullptr

void functionA(std::unique_ptr<Rectangle> ptr)

functionAはstd::unique_ptr<Rectangle>を引数として値渡しで受け取ります
functionAを呼び出す際、`functionA(p1)`と記述すると、std::unique_ptrはコピーができないため、コンパイルエラーになります

// 値渡しでfunctionAを呼び出
functionA(std::move(p1));

そのため、上記のように`std::move(p1)`を引数にすることで、p1 の所有権が functionA の引数 ptr に移動します
std::move が呼ばれた後、main 関数の p1 は、Rectangle オブジェクトを所有していません

functionA 内で ptr が使用され、functionA が終了すると、functionA のローカル変数である ptrが破棄されます
この時、ptrが所有していた Rectangle オブジェクトも自動的に破棄(delete)されます

main 関数に戻ると、元の p1 はすでに所有権を失っているため、元の p1 は何も指さなくなり nullptr になります
一方、参照呼び出しの関数を定義してみましょう

 void functionB(std::unique_ptr<Rectangle>& ptr){
    std::cout << "in functionB" << std::endl;
 }

 int main() {
     // スマートポインタを宣言し、new でオブジェクトを作成して渡す
     std::unique_ptr<Rectangle> p1(new Rectangle(10, 5));
     // -> でオブジェクトのメソッドを呼ぶ
     std::cout << "Area: " << p1->area() << std::endl;

     // 参照渡しでfunctionBを呼び出す
     functionB(p1);

     // p1 は関数呼び出し前同様に、Rectangleオブジェクトを指している
     if (p1 == nullptr) {
         std::cout << "p1 is nullptr" << std::endl;
     }else{
          std::cout << "Area: " << p1->area() << std::endl;
     }
     return 0;
}
実行結果
Area: 50
in functionB
Area: 50

void functionB(std::unique_ptr<Rectangle>& ptr)

functionB は std::unique_ptr<Rectangle> への参照を引数として受け取ります

// 参照渡しでfunctionBを呼び出す
functionB(p1);

main 関数で functionB(p1) が呼び出されると、main 関数の p1自身への参照が functionB に渡されます
つまり、functionB内では main 関数の p1 を直接操作し、所有権の移動は起こりません
functionB 内で ptr が使用されますが、functionB が終了しても、main 関数の p1 は元の Rectangle オブジェクトの所有権を持ち続けます
したがって、main 関数に戻った後も、p1 は有効な Rectangle オブジェクトを指しています

  1. std::shared_ptr
    共有される動的に割り当てられたリソースを指すスマートポインタです
    複数の std::shared_ptr が同じリソースを所有することができ、内部カウンタで参照数を管理します
    最後の std::shared_ptr が破棄されると、リソースは自動的に解放されます
    以下は、shared_ptrを使用したサンプルプログラムで、shared_ptrの特性を検証しています
int main() {
    // スマートポインタを宣言し、new でオブジェクトを作成して渡す
    std::shared_ptr<Rectangle> p1(new Rectangle(10, 5));
    // 参照カウンタは1
    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    // コピーで別のスマートポインタに所有権を共有する
    std::shared_ptr<Rectangle> p2 = p1;
    // 参照カウンタは2
    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    std::cout << "p2 use count: " << p2.use_count() << std::endl;
    // -> でオブジェクトのメソッドを呼ぶ
    std::cout << "Area: " << p1->area() << std::endl;
    // p1 と p2 がスコープから外れると、オブジェクトは削除される
    return 0;
}
実行結果
p1 use count: 1
p1 use count: 2
p2 use count: 2
Area: 50


p1.use_count()はスマートポインタの参照数を取得する関数です

std::shared_ptr<Rectangle> p1(new Rectangle(10, 5));
// 参照カウンタは1
std::cout << "p1 use count: " << p1.use_count() << std::endl;

この行は、std::shared_ptr を使って動的にメモリを確保し、そのオブジェクトの所有権を p1 に持たせています
具体的には、new Rectangle(10, 5) でヒープメモリ上に Rectangle オブジェクトが作成され、そのオブジェクトの管理を p1 が引き受けます
p1がRectangleオブジェクトを指しているため、参照カウンタは1です

// コピーで別のスマートポインタに所有権を共有する
std::shared_ptr<Rectangle> p2 = p1;
// 参照カウンタは2
std::cout << "p1 use count: " << p1.use_count() << std::endl;
std::cout << "p2 use count: " << p2.use_count() << std::endl;

p2ポインタを用意し、p1が指すRectangleオブジェクトをコピーしています
これにより、p1、p2がRectangleオブジェクトを指しているので、両方とも参照カウンタは2になります

スマートポインタはスコープを抜けると自動的に削除されるので、p1、p2とも削除されメモリから解放されます

次に、関数の引数に指定した場合の動作をみてみましょう

void functionA(std::shared_ptr<Rectangle> ptr){
    // 参照カウンタは2
    std::cout << "in functionA p1 use count:" << ptr.use_count() << std::endl;

    // ptr がスコープから外れると、オブジェクトは削除される
}

int main() {
    // スマートポインタを宣言し、new でオブジェクトを作成して渡す
    std::shared_ptr<Rectangle> p1(new Rectangle(10, 5));
    // 参照カウンタは1
    std::cout << "p1 use count: " << p1.use_count() << std::endl;

    functionA(p1);

    // 参照カウンタは1
    std::cout << "p1 use count: " << p1.use_count() << std::endl;

    // -> でオブジェクトのメソッドを呼ぶ
    std::cout << "Area: " << p1->area() << std::endl;
    // p1 がスコープから外れると、オブジェクトは削除される
    return 0;
}
実行結果
p1 use count: 1
in functionA p1 use count:2
p1 use count: 1
Area: 50

   void functionA(std::shared_ptr<Rectangle> ptr)

functionAはstd::unique_ptr<Rectangle>を引数として値渡しで受け取ります

   functionA(p1);

main関数からfunctionAを呼び出すと、リソースの所有権はfunctionA()にコピーされ、所有数も2となります
functionA 内で ptr が使用され、関数が終了すると、functionA() のローカル変数である ptr が破棄され、Rectangle オブジェクトも自動的に破棄(delete)されます
main 関数に戻ると、元のptr は残っているため、所有数は1となります

一方、スマートポインタを参照で渡すfunctionBを呼び出すと以下のような実行結果になります

void functionB(std::shared_ptr<Rectangle>& ptr){
        std::cout << "in functionB p1 use count:" << ptr.use_count() << std::endl;
    }

    int main() {
        // スマートポインタを宣言し、new でオブジェクトを作成して渡す
        std::shared_ptr<Rectangle> p1(new Rectangle(10, 5));
        // 参照カウンタは1
        std::cout << "p1 use count: " << p1.use_count() << std::endl;

        functionB(p1);

        // 参照カウンタは1
        std::cout << "p1 use count: " << p1.use_count() << std::endl;

        std::cout << "Area: " << p1->area() << std::endl;
        // p1 がスコープから外れると、オブジェクトは削除される
        return 0;
    }
実行結果
p1 use count: 1
    in functionB p1 use count:1
    p1 use count: 1
    Area: 50

void functionB(std::shared_ptr<Rectangle>& ptr)

functionB は std::unique_ptr<Rectangle> への参照を引数として受け取ります

functionB(p1);

main 関数で functionB(p1) が呼び出されると、main 関数の p1 自身への参照が functionB に渡されます
つまり、functionB は main 関数の p1 を直接操作し、所有権の移動、コピーは起こりません
functionB 内で ptr が使用されますが、functionB が終了しても、main 関数の p1 は元の Rectangle オブジェクトの所有権を持ち続けます
したがって、main 関数に戻った後も、p1 は有効な Rectangle オブジェクトを指しており、参照カウンタも1となります

  1. std::weak_ptr
    std::shared_ptr と同じリソースを指すスマートポインタですが、参照カウンタを増やしません
    参照カウンタを増やさないことが、循環参照を防ぐのに役立ちます

循環参照とは、複数のオブジェクトが互いに参照しあう状態になることです

通常の std::shared_ptr は、そのオブジェクトが参照されるたびに、内部の参照カウンタを増やします
そして、スコープを抜けるとき、この参照カウンタが0になれば、自動的にそのオブジェクトをメモリから解放します
しかし、循環参照が発生すると、たとえばオブジェクトAがオブジェクトBを、オブジェクトBがオブジェクトAを指しているような場合、お互いの参照カウンタがいつまでたってもゼロになりません
たとえそれらのオブジェクトがもう使われていなくても、参照カウンタがゼロにならないため、コンピューターはそれらを解放できずメモリリークを引き起こしてしまいます
std::weak_ptr は参照カウンタを増やさないため、循環参照の一方を std::weak_ptr で持つことで、お互いの参照カウンタがゼロにならない状態を解消し、正しくメモリが解放されるように改善できます

以下は、自分自身を参照するstd::shared_ptrを使用したサンプルプログラムです

#include <iostream>
#include <memory>

class Rectangle {
private:
    int length;
    int breadth;
    std::shared_ptr<Rectangle> self; // 自分自身を指すスマートポインタ
public:
    Rectangle(int l, int b) {
        length = l;
        breadth = b;
    }
    ~Rectangle(){
        std::cout << "delete" << std::endl;
    }

    int area() {
        return length * breadth;
    }
    void setSelf(std::shared_ptr<Rectangle> ptr) {
        self = ptr; // 自分自身を指すスマートポインタをセットする
    }
};

int main() {
    // スマートポインタを宣言し、new でオブジェクトを作成して渡す
    std::shared_ptr<Rectangle> p1(new Rectangle(10, 5));
    // 自分自身を指すスマートポインタをセットする
    p1->setSelf(p1);
    // 参照カウンタは2 (自分自身を指すスマートポインタが1つ増える)
    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    // -> でオブジェクトのメソッドを呼ぶ
    std::cout << "Area: " << p1->area() << std::endl;
    // p1 がスコープから外れても、オブジェクトは削除されない (循環参照が発生する)
    // これを防ぐには、自分自身を指すスマートポインタを std::weak_ptr にする
    return 0;
}
実行結果
p1 use count: 2
Area: 50

std::shared_ptr<Rectangle> self; // 自分自身を指すスマートポインタ

自分自身を指すポインタをstd::shared_ptrで宣言しています

// 自分自身を指すスマートポインタをセットする
p1->setSelf(p1);
// 参照カウンタは2 (自分自身を指すスマートポインタが1つ増える)
std::cout << "p1 use count: " << p1.use_count() << std::endl;

main 関数内で p1->setSelf(p1) を呼び出した際に、p1 が Rectangle オブジェクトを指し、その Rectangle オブジェクト内の self もまた p1 と同じ Rectangle オブジェクトを指すことになります

これにより、以下の状態が生まれます

  • 外部からの参照: p1 が Rectangle オブジェクトを指しています
  • 内部からの参照: Rectangle オブジェクト内の self が、自分自身(Rectangle オブジェクト)を指しています

この結果、p1 がスコープを抜けても、Rectangle オブジェクト内の self が自身を参照し続けるため、参照カウンタがゼロにならず、デストラクタが呼び出されませんでした

次に、自分自身のポインタをstd::weak_ptrに書き換えてみましょう

#include <iostream>
#include <memory>

class Rectangle {
private:
    int length;
    int breadth;
    std::weak_ptr<Rectangle> self; // 自分自身を指すスマートポインタ
public:
    Rectangle(int l, int b) {
        length = l;
        breadth = b;
    }
    ~Rectangle(){
        std::cout << "delete" << std::endl;
    }

    int area() {
        return length * breadth;
    }
    void setSelf(std::shared_ptr<Rectangle> ptr) {
        self = ptr; // 自分自身を指すスマートポインタをセットする
    }
};

int main() {
    // スマートポインタを宣言し、new でオブジェクトを作成して渡す
    std::shared_ptr<Rectangle> p1(new Rectangle(10, 5));
    // 自分自身を指すスマートポインタをセットする
    p1->setSelf(p1);
    // 参照カウンタは1 (参照カウンタに変化はない)
    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    // -> でオブジェクトのメソッドを呼ぶ
    std::cout << "Area: " << p1->area() << std::endl;
    // p1 がスコープから外れたとき、オブジェクトが削除される
    return 0;
}
実行結果
p1 use count: 1
Area: 50
delete

// 自分自身を指すスマートポインタをセットする
p1->setSelf(p1);
// 参照カウンタは1 (参照カウンタに変化はない)
std::cout << "p1 use count: " << p1.use_count() << std::endl;

self を std::weak_ptr に変更したことで、self が Rectangle オブジェクトを参照しても、その参照が参照カウンタにカウントされなくなります
そのため、p1 がスコープを抜けて参照がなくなると、Rectangle オブジェクトの参照カウンタはゼロになり、正しくデストラクタが呼び出されてメモリが解放されるようになりました

コメント

この記事へのコメントはありません。

関連記事

Python 応用編 5日目

C++ 発展編 3日目

C言語 応用編 ~3日目~

PAGE TOP