C++のスコープとunique_ptr

C++ コンピュータ
C++

unique_ptrについて学習しています。

スタックに生成したオブジェクト

newではなくクラスを宣言だけするとオブジェクトはメモリのスタック領域に配置されます。

スタックにクラスのインスタンスを生成するとスコープ(この場合main関数のブロック{})を抜けるとデストラクトで解放されます。

#include <iostream>
class SampleClass
{
public:
    SampleClass() {
        std::cout << "SampleClass()" << std::endl;
    }
    ~SampleClass() {
        std::cout << "~SampleClass()" << std::endl;
    }
};

int main() {
    
    SampleClass obj;
    
    return 0;
}
/*
プログラム出力
SampleClass()
~SampleClass()
*/

ヒープに生成したオブジェクト解放されない例

newで生成されたオブジェクトはメモリのヒープ領域に配置されます。

この場合、スコープを抜けてもデストラクタが呼ばれるとは無い様なので解放されていません。(メモリリーク?)

#include <iostream>
class SampleClass
{
public:
    SampleClass() {
        std::cout << "SampleClass()" << std::endl;
    }
    ~SampleClass() {
        std::cout << "~SampleClass()" << std::endl;
    }
};

int main() {
    
    SampleClass* ptr = new SampleClass();
    
    return 0;
}
/*
プログラム出力
SampleClass()
*/

ヒープに生成したオブジェクト解放した例

newしたらdeleteする必要があります。

#include <iostream>
class SampleClass
{
public:
    SampleClass() {
        std::cout << "SampleClass()" << std::endl;
    }
    ~SampleClass() {
        std::cout << "~SampleClass()" << std::endl;
    }
};

int main() {
    
    SampleClass* ptr = new SampleClass();
    delete ptr;
    return 0;
}
/*
プログラム出力
SampleClass()
~SampleClass()
*/

作成したコードが確実にdeleteされているか不安が付きまといます。
(多量に変数を使っていて例外が発生した場合など、全てが漏れなくdelete出来ているか?)

std::unique_ptrを使い自動的に開放

std::unique_ptrを使うことでヒープにクラスのインスタンスを生成してもスコープを抜けると自動的にデストラクトされます。

#include <iostream>
#include <cstdlib>
#include <memory>
class SampleClass
{
public:
    SampleClass() {
        std::cout << "SampleClass()" << std::endl;
    }
    ~SampleClass() {
        std::cout << "~SampleClass()" << std::endl;
    }
};

int main() {
    
    std::unique_ptr<SampleClass> ptr(new SampleClass());
    return 0;
}
/*
プログラム出力
SampleClass()
~SampleClass()
*/

デストラクタが呼ばれていることが確認出来ます。

std::moveによる所有権の移転

#include <iostream>
#include <memory>
int main() {
    std::unique_ptr<int []> p(new int[] {1,2,3});

    if (p != nullptr)
        std::cout << p[0] << std::endl;

    std::unique_ptr<int []> p2 = std::move(p);

    if (p == nullptr)
        std::cout << "p is null pointer" << std::endl;
    
    return 0;
}
/*
プログラム出力
1
p is null pointer
*/

サンプルコードではstd::moveで所有権をpからp2移転したためpはnullになっています。

クラスのメンバ変数でstd::unique_ptr

#include <iostream>
#include <memory>

class InnerClass
{
public:
    ~InnerClass() {
        std::cout << "~InnerClass()" << std::endl;
    }
};
class SampleClass
{
private:
    std::unique_ptr<InnerClass> _p;
public:
    SampleClass(){
        _p = std::make_unique<InnerClass>();
    }
};

int main() {
    std::unique_ptr<SampleClass> p(new SampleClass);
    
    return 0;
}
/*
プログラム出力
~InnerClass()
*/

所属するオブジェクトの解放でメンバ変数も自動的にデストラクタが呼ばれていることが確認出来る。

関数の引数にstd::unique_ptrを使う場合

#include <iostream>
#include <memory>

void process(std::unique_ptr<int> ptr) {
  // ptr を使って処理を行う
  std::cout << *ptr << std::endl;
}

int main() {
  std::unique_ptr<int> ptr = std::make_unique<int>(10);
  process(std::move(ptr)); // ptr の所有権を process 関数に譲渡
  // ptr は空になっている
  return 0;
}
/*
プログラム出力
10
*/

呼び出したprocess()関数に所有権が移譲されたため、process()関数が終了するとptrは解放される。

どのような状況で使うのか思いつきません。

std::unique_ptr<int>から生ポインタを取り出す

#include <iostream>
#include <memory>

void process(int* ptr) {
  // ptr を使って処理を行う (所有権は変更しない)
  std::cout << *ptr << std::endl;
}

int main() {
  std::unique_ptr<int> ptr = std::make_unique<int>(10);
  process(ptr.get()); // 生ポインターを渡す
  // ptr は所有権を保持したまま
  return 0;
}
/*
プログラム出力
10
*/

ptr.get()で生ポインターを取り出すことが出来るようです。
関数内で書き換えも出来てしまうので使わないような設計にした方が良さそう。
引数にconstをつけて書換不可にすれば罪が軽いか?

std::unique_ptrの参照渡し

#include <iostream>
#include <memory>

void modify(std::unique_ptr<int>& ptr) {
  // ptr が指すオブジェクトを変更
  *ptr = 20;
}

int main() {
  std::unique_ptr<int> ptr = std::make_unique<int>(10);
  modify(ptr); // 参照渡し
  std::cout << *ptr << std::endl; // 20
  return 0;
}
/*
プログラム出力
20
*/

関数内で書き換えも出来て、所有権はmain()のスコープのままです。
こちらは使い勝手が良さそうです。

 

感想

安全にポインタを使えるようになるということでunique_ptrには期待したのですが、筆者には難しすぎたようです。

ポインタというと配列のアドレスをポインタ変数に代入して、ポインタ変数をインクリメントしながら配列の要素を書き換えるような使い方を想像してしましますが、unique_ptrはそのようなことは出来ない感じがします。

unique_ptrの所有権の移譲については、余り馴染みが無いのですが、同じアドレスを指すポインタの存在を許さない仕組みなのですかね?基本的にunique_ptrのみで成立するようなプログラムを組むと、トラブルが少なそうな予感がします。ただ場当たり的なコードを書くことが出来ないので、少し窮屈にも感じます。

コメント