🖥️

【C++言語入門】 第26回 スマートポインタ(unique_ptr)

2025/04/13に公開

https://youtu.be/yJAHirnz2og

四国めたん
\textcolor{pink}{四国めたん: }教師役ですわ

ずんだもん
\textcolor{lime}{ずんだもん: }生徒役なのだ

\footnotesize \textcolor{pink}{四国めたん:} こんにちは。四国めたんです

\footnotesize \textcolor{lime}{ずんだもん:} ずんだもんなのだ。こんにちはなのだ

\footnotesize \textcolor{pink}{四国めたん:} 今回から数回に分けて スマートポインタ についてお話しをしていきますわ

\footnotesize \textcolor{lime}{ずんだもん:} スマートポインタ

\footnotesize \textcolor{pink}{四国めたん:} newで確保したメモリ領域を管理して、不必要になったら自動的に開放する機能ですわ

\footnotesize \textcolor{lime}{ずんだもん:} お~、便利そうなのだ

メモリ管理にスマートポインタを

\footnotesize \textcolor{pink}{四国めたん:} さて、前回までメモリ領域の確保と云えば、mallocnewを使ってきましたわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ、freedeleteを使ってメモリ領域を開放するのが面倒だったのだ

\footnotesize \textcolor{pink}{四国めたん:} メモリ領域の開放を忘れると、最悪、コンピューターのクラッシュなどの不具合が生じますから...

\footnotesize \textcolor{lime}{ずんだもん:} こういった面倒なところがC言語やC++言語の悪い部分なのだ

\footnotesize \textcolor{pink}{四国めたん:} たしかに、他の言語、たとえばJavaPythonなどでは、使用しなくなったメモリ領域は自動的に開放されますわね

\footnotesize \textcolor{lime}{ずんだもん:} メモリ領域の開放を気にしなくてもいいということは、かなりのストレス軽減になるのだ

\footnotesize \textcolor{pink}{四国めたん:} そこで、C++言語では スマートポインタ と云う仕組みを導入しましたわ

\footnotesize \textcolor{lime}{ずんだもん:} どのような仕組みなのだ?

\footnotesize \textcolor{pink}{四国めたん:} スマートポインタ は、標準テンプレートライブラリ(STL)で実装されたクラスによって実現されていますわ

\footnotesize \textcolor{lime}{ずんだもん:} つまり テンプレートクラス のひとつなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、現在、unique_ptr<>shared_ptr<>weak_ptr<>の3つが主流ですわね

\footnotesize \textcolor{lime}{ずんだもん:} 詳しく教えて欲しいのだ

auto_ptr<>

C++98では既に スマートポインタ が導入されています

ISO/IECにC++言語が登録され、標準テンプレートライブラリがサポートされたのもC++98からです

その時の スマートポインタ をサポートするテンプレートクラスはauto_ptr<>でした

auto_ptr<>は、余り洗練されておらず、C++11では非推奨に、C++17では削除されました

その代わりとして、より洗練された スマートポインタ であるunique_ptr<>shared_ptr<>weak_ptr<>が導入されました

新しく作成するプログラムでは、これらの新しい スマートポインタ を使っていきましょう。

unique_ptr<>

\footnotesize \textcolor{pink}{四国めたん:} 最初はunique_ptr<>ですわ

\footnotesize \textcolor{lime}{ずんだもん:} unique_ptr<>

\footnotesize \textcolor{pink}{四国めたん:} はい、正式にはこのような宣言になりますわ

namespace std {
  template <class T, class D = std::default_delete<T>>
  class unique_ptr;
}

\footnotesize \textcolor{pink}{四国めたん:} なお、ヘッダーファイルとして"memory"をインクルードする必要がありますわ

\footnotesize \textcolor{lime}{ずんだもん:} わかったのだ

\footnotesize \textcolor{pink}{四国めたん:} まず、unique_ptr<>は"T"型のメモリ領域へのポインタを保持しますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

\footnotesize \textcolor{pink}{四国めたん:} メモリ領域が不要になったら"D"にセットされた ディリータ を使って開放しますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} なお、"T"型のインスタンスは、基本的にはnewにより生成されている必要がありますわ

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

\footnotesize \textcolor{lime}{ずんだもん:} ところで ディリータ とはなんなのだ?

\footnotesize \textcolor{pink}{四国めたん:} ディリータ とはdeleteなど、メモリ領域を開放するための演算子のことですわ

\footnotesize \textcolor{lime}{ずんだもん:} わかったのだ

\footnotesize \textcolor{lime}{ずんだもん:} ところでstd::default_delete<>はなんなのだ?

\footnotesize \textcolor{pink}{四国めたん:} std::default_delete<>は、 ディリータ を管理するクラスですわ

\footnotesize \textcolor{pink}{四国めたん:} 通常はデフォルトのままでOKですわ

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

\footnotesize \textcolor{pink}{四国めたん:} unique_ptr<>の基本的な使い方はこのようになりますわ

std::unique_ptr<クラス名> 変数名(インスタンスへのポインタ);

\footnotesize \textcolor{pink}{四国めたん:} クラス名には、unique_ptr<>で管理、と云うか保持するメモリ領域のクラスや型の名前を指定しますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

\footnotesize \textcolor{pink}{四国めたん:} また、メモリ領域へのポインタは、通常、その場でnewを使って確保したメモリ領域を指定することをお勧めしますわ

\footnotesize \textcolor{lime}{ずんだもん:} なぜなのだ?

\footnotesize \textcolor{pink}{四国めたん:} あとで割り当てようとすると、忘れてしまう場合が多いからですわ

\footnotesize \textcolor{lime}{ずんだもん:} なっとくなのだ

\footnotesize \textcolor{pink}{四国めたん:} ちなみに、unique_ptr<>として宣言されたインスタンスは、名前の通り、ポインタのように扱えますわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} 例えば、間接演算子"*"を使えば保持するインスタンスそのものにアクセスすることができますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむ

\footnotesize \textcolor{pink}{四国めたん:} また、保持するメモリ領域のクラスのメソッドを呼び出すには、アロー演算子->が使えますわね

*変数名;
変数名->メソッド名(引数...);

\footnotesize \textcolor{lime}{ずんだもん:} なるほど、保持しているメモリ領域へのポインタのように扱えるのか...

\footnotesize \textcolor{pink}{四国めたん:} とりあえずCircleクラスをunique_ptr<>で管理してみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;
const int kSize = 50;

/// @brief 円
class Circle {
  double diameter_;  // 直径
  char* pmessage_;

 public:
  Circle() : Circle(10.0) {}
  Circle(double diameter) : diameter_(diameter), pmessage_(nullptr) {
    pmessage_ = new char[kSize];
  }
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  }

  double Diameter() const { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual const char* Message() const noexcept {
    auto area = [this]() {
      double radius = this->Diameter() / 2;
      double a = radius * radius * kPI;
      return a;
    };
    sprintf_s(pmessage_, kSize, "円の面積は%fです。", area());
    return pmessage_;
  }
};

#endif  // CIRCLE_H
hello_world.cpp
#include <iostream>
#include <memory>
#include "circle.h"

int main(int argc, char* argv[]) {
  std::unique_ptr<Circle> c(new Circle(10.0));
  std::cout << c->Message() << std::endl;
  return 0;
}

ユニークポインタ

\footnotesize \textcolor{pink}{四国めたん:} まず、メイン関数の最初でunique_ptr<>のテンプレート引数としてCircleを指定していますわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} また、宣言した変数"c"の初期値としてnew Circle(10.0)で生成したポインタを指定していますわ

\footnotesize \textcolor{lime}{ずんだもん:} 生成してすぐにunique_ptr<>の初期値として渡しているのだ

\footnotesize \textcolor{pink}{四国めたん:} そして、CircleクラスのMessageメソッドを呼び出すには、変数"c"に対してアロー演算子->を使いますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} ちなみに間接演算子"*"を使って(*c).Message()としてもOKですわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

\footnotesize \textcolor{pink}{四国めたん:} そして、メイン関数の最後にdeleteによってCircleクラスのインスタンスを明示的に破棄していませんわね

\footnotesize \textcolor{lime}{ずんだもん:} unique_ptr<>によってメイン関数を抜ける際に自動的に破棄されるのだ

\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

配列を保持するunique_ptr<>

\footnotesize \textcolor{lime}{ずんだもん:} ところでunique_ptr<>は配列を保持することはできるのか?

\footnotesize \textcolor{pink}{四国めたん:} できますわね

\footnotesize \textcolor{lime}{ずんだもん:} その辺りを教えて欲しいのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、正式にはこのような宣言になりますわ

namespace std {
  template <class T, class D>
  class unique_ptr<T[], D>;
}

\footnotesize \textcolor{lime}{ずんだもん:} なんか複雑なのだ

\footnotesize \textcolor{pink}{四国めたん:} 基本的な使い方はこのようになりますわ

std::unique_ptr<クラス名[]> 変数名(配列へのポインタ);

\footnotesize \textcolor{lime}{ずんだもん:} テンプレート引数のクラス名に配列を示す角括弧"[]"を付加すればいいのか?

\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

\footnotesize \textcolor{pink}{四国めたん:} ちなみに、 ディリータ "D"は省略できますし、通常は省略しますわね

\footnotesize \textcolor{lime}{ずんだもん:} わかったのだ

\footnotesize \textcolor{pink}{四国めたん:} なお、配列を保持するunique_ptr<>で宣言されたインスタンスは、通常の配列のように扱えますわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} 例えば、保持する配列の要素は角括弧"[]"を使ってアクセスできますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} また、配列の要素のメソッドを呼び出すには、ドット演算子.を使用できますわ

変数名[インデックス].メソッド名(引数...);

\footnotesize \textcolor{lime}{ずんだもん:} お~、便利なのだ

\footnotesize \textcolor{pink}{四国めたん:} それではメイン関数中でCircleの配列をunique_ptr<>に割り当ててみましょう

hello_world.cpp
#include <iostream>
#include <memory>
#include "circle.h"

int main(int argc, char* argv[]) {
  std::unique_ptr<Circle[]> c(new Circle[2]);
  c[1].Diameter(5.0);
  std::cout << c[0].Message() << std::endl;
  std::cout << c[1].Message() << std::endl;
  return 0;
}

配列のユニークポインタ

\footnotesize \textcolor{lime}{ずんだもん:} とりあえず要素数は2になっているのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、テンプレート引数はCircle[]として、配列であることを明示していますわ

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

\footnotesize \textcolor{pink}{四国めたん:} なお、配列のメモリ領域を確保する場合は、デフォルトコンストラクタが呼ばれますわ

\footnotesize \textcolor{lime}{ずんだもん:} そうだったのだ

\footnotesize \textcolor{pink}{四国めたん:} 今回、Circleのデフォルトコンストラクタでは、メンバ変数"diameter_"に10.0を設定していますわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} そして"c[1]"の直径をDiameterメソッドで5.0に変更していますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむ

\footnotesize \textcolor{pink}{四国めたん:} なお、unique_ptr<>に保持された配列の要素を指定する場合、"c[0]"や"c[1]"のように、角括弧"[]"にインデックスを指定しますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} そして、各要素のメソッドは、c[0].Message()のように、ドット演算子.を用いて呼び出せますわ

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

クラスのメンバ変数にunique_ptr<>

\footnotesize \textcolor{lime}{ずんだもん:} ところでクラスのメンバ変数にunique_ptr<>を使えるのか?

\footnotesize \textcolor{pink}{四国めたん:} 使えますわね

\footnotesize \textcolor{pink}{四国めたん:} その場合、コンストラクタでメモリ領域を確保、保持するのがお勧めですわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} もちろん、後でメソッドなどを使ってメモリ領域を割り当ててもOKですわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} いずれにしても、クラスのインスタンスが破棄される際に、自動的にメモリ領域を開放してくれますわ

\footnotesize \textcolor{lime}{ずんだもん:} 今までのように、デストラクタで明示的に開放する必要がなくなるのだ

\footnotesize \textcolor{pink}{四国めたん:} とりあえずCircleクラスの"pmessage_"をunique_ptr<>を使うように書き換えてみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>
#include <memory>

const double kPI = 3.14159265358979323846;
const int kSize = 50;

/// @brief 円
class Circle {
  double diameter_;  // 直径
  std::unique_ptr<char[]> pmessage_;

 public:
  Circle() : Circle(10.0) {}
  Circle(double diameter) : diameter_(diameter), pmessage_(new char[kSize]) {}
  virtual ~Circle() {}

  double Diameter() const { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual const char* Message() const noexcept {
    auto area = [this]() {
      double radius = this->Diameter() / 2;
      double a = radius * radius * kPI;
      return a;
    };
    sprintf_s(pmessage_.get(), kSize, "円の面積は%fです。", area());
    return pmessage_.get();
  }
};

#endif  // CIRCLE_H

クラス中のユニークポインタ

\footnotesize \textcolor{lime}{ずんだもん:} 説明をお願いするのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、まず"pmessage_"の型をchar*からstd::unique_ptr<char[]>に変えていますわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} テンプレート引数はchar[]として、配列を明示していますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} そして、コンストラクタの初期化子リストで"pmessage_"にchar型の配列を確保して渡していますわ

\footnotesize \textcolor{lime}{ずんだもん:} だからコンストラクタとデストラクタでのメモリ領域の確保と開放が削除されているのだ

get()メソッド

\footnotesize \textcolor{lime}{ずんだもん:} ところでsprintf_s関数の第1引数がpmessage_.get()となっているのだが...

\footnotesize \textcolor{pink}{四国めたん:} はい、Messageメソッド内のsprintf_s関数の第1引数は、char型のメモリ領域へのポインタが必要ですわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} なので、unique_ptr<>から保持しているメモリ領域へのポインタを取得するメソッドgetを使用していますわね

\footnotesize \textcolor{lime}{ずんだもん:} Messageメソッドの戻り値に対しても、getメソッドで保持しているメモリ領域へのポインタを返しているのだ

\footnotesize \textcolor{pink}{四国めたん:} そうですわね

\footnotesize \textcolor{pink}{四国めたん:} ちなみにgetメソッドは単にポインタを返すだけなので、unique_ptr<>のインスタンスには影響を与えませんわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} ですので、Circleクラスのインスタンスが破棄された時点で、getメソッドで取得したメモリ領域は破棄されますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむ

\footnotesize \textcolor{pink}{四国めたん:} 従来であれば、deleteなどを使用して明示的に破棄していたので問題はありませんでしたわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかにその通りなのだ

\footnotesize \textcolor{pink}{四国めたん:} しかしunique_ptr<>では、インスタンスの破棄は自動でおこなわれますわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} 破棄されるタイミングは見極めないと、getメソッドで取得したメモリ領域が既に破棄されていることにもなりかねませんわ

\footnotesize \textcolor{lime}{ずんだもん:} 注意が必要なのだ

まとめ

\footnotesize \textcolor{pink}{四国めたん:} お疲れさまでした

\footnotesize \textcolor{lime}{ずんだもん:} おつかれさまなのだ

\footnotesize \textcolor{pink}{四国めたん:} 以上で スマートポインタ(unique_ptr) を一旦終了しますわ

\footnotesize \textcolor{lime}{ずんだもん:} 一旦?

\footnotesize \textcolor{pink}{四国めたん:} はい、unique_ptr<>には、まだまだお話しすることが多いですわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} ですので、次回にそれらをお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} よろしくなのだ

Discussion