🖥️

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

に公開

https://youtu.be/WddPHRbyEHk

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} 今回はshared_ptr<>についてお話ししますわ

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

\footnotesize \textcolor{pink}{四国めたん:} スマートポインタ のひとつで、unique_ptr<>と、似ていますわね

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

shared_ptr<>

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

namespace std {
  template <class T>
  class shared_ptr;
}

\footnotesize \textcolor{lime}{ずんだもん:} あまりピンとこないのだ

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

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

\footnotesize \textcolor{pink}{四国めたん:} クラス名には、shared_ptr<>で管理、と云うか保持するインスタンスのクラスの名前を指定しますわ

\footnotesize \textcolor{lime}{ずんだもん:} この辺りはunique_ptr<>と一緒なのだ

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} ちなみに、shared_ptr<>のインスタンスは、unique_ptr<>と同様にポインタと同じように扱えますわ

\footnotesize \textcolor{lime}{ずんだもん:} つまり間接演算子"*"やアロー演算子->でインスタンスやメソッドにアクセスできるということか

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

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえずCircleクラスをshared_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::shared_ptr<Circle> c(new Circle(10.0));
  std::cout << c->Message() << std::endl;
  return 0;
}

シェアードポインタ

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

\footnotesize \textcolor{lime}{ずんだもん:} 宣言した変数"c"の初期値としてnew Circle(10.0)で生成したメモリ領域を指定しているのだ

\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}{ずんだもん:} shared_ptr<>によってメイン関数を抜ける際に自動的に破棄されるのだ

配列を保持するshared_ptr<>

\footnotesize \textcolor{pink}{四国めたん:} shared_ptr<>は配列を保持することもできますわ

\footnotesize \textcolor{lime}{ずんだもん:} この点もunique_ptr<>と一緒なのだ

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

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

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} ちなみに、配列を保持するshared_ptr<>のインスタンスは、通常の配列のように扱えますわ

\footnotesize \textcolor{lime}{ずんだもん:} つまり、保持する配列の要素は角括弧"[]"を使ってアクセスできると云うことか

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

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

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

コンパイラをC++17以降に変えましょう

\footnotesize \textcolor{pink}{四国めたん:} ところで、shared_ptr<>に、そのまま配列を保持できるようになったのは、正式にはC++17以降ですわ

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

\footnotesize \textcolor{lime}{ずんだもん:} でも、Visual Studio 2022は標準でC++14コンパイラを使用しているのではないのか?

\footnotesize \textcolor{pink}{四国めたん:} ですので、とりあえずコンパイラをC++17以降に変更してみましょう

  1. ソリューションエクスプローラー のプロジェクト名を右クリック
  2. メニューから プロパティ を選択
  3. 構成:全ての構成 に、 プラットフォーム:全てのプラットフォーム に指定
  4. プロパティページ全般プロパティC++言語標準ISO C++20 標準 を選択

\footnotesize \textcolor{pink}{四国めたん:} まず、 ソリューションエクスプローラー でプロジェクト名"Project2"を右クリックしますわ

\footnotesize \textcolor{lime}{ずんだもん:} メニューが出てきたのだ

\footnotesize \textcolor{pink}{四国めたん:} 一番下の プロパティ を選択しますわ

\footnotesize \textcolor{lime}{ずんだもん:} プロパティページ ダイアログが表示されたのだ

\footnotesize \textcolor{pink}{四国めたん:} 構成:全ての構成 に、 プラットフォーム:全てのプラットフォーム になっていることを確認しますわ

\footnotesize \textcolor{lime}{ずんだもん:} 違っていたら直しておくのだ

\footnotesize \textcolor{pink}{四国めたん:} C++言語標準 の項目を ISO C++20 標準 に変更しますわ

\footnotesize \textcolor{lime}{ずんだもん:} ISO C++17 標準 でなくてもOKなのか?

\footnotesize \textcolor{pink}{四国めたん:} いい機会ですので、最新のものを指定した方がいいですわ

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

プロパティページ

shared_ptr<>に配列を使ってみましょう

\footnotesize \textcolor{pink}{四国めたん:} 準備もできたので、メイン関数中でCircleの配列をshared_ptr<>に割り当ててみましょう

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

int main(int argc, char* argv[]) {
  std::shared_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}{ずんだもん:} Circleのデフォルトコンストラクタでは、メンバ変数"diameter_"に10.0を設定しているのだ

\footnotesize \textcolor{pink}{四国めたん:} "c[1]"の直径を5.0にするために、ドット演算子"."でDiameterメソッドを使いましたわ

\footnotesize \textcolor{lime}{ずんだもん:} shared_ptr<>に保持された配列の要素を指定するために"c[0]"や"c[1]"のように、角括弧"[]"にインデックスを指定しているのだ

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} unique_ptr<>と同様に、クラスのメンバ変数にshared_ptr<>を使うこともできますわ

\footnotesize \textcolor{lime}{ずんだもん:} その場合、コンストラクタでメモリ領域を確保、保持するのが良いのか?

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

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえずCircleクラスの"pmessage_"をshared_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::shared_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::shared_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}{四国めたん:} なので、shared_ptr<>から保持しているメモリ領域へのポインタを取得するメソッドgetを使用していますわね

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

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

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

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

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

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

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

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

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

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

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

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

まとめ

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} unique_ptr<>shared_ptr<>は概ね似たような機能を提供しますわ

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

\footnotesize \textcolor{pink}{四国めたん:} ただ、違いもあるので、その辺りを次回にお話ししますわ

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

Discussion