🖥️

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

に公開

https://youtu.be/Bor5axHqBPw

四国めたん
\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<>shared_ptr<>の最大の違いをお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} どのような違いなのだ?

\footnotesize \textcolor{pink}{四国めたん:} 実は、shared_ptr<>のインスタンスは別のshared_ptr<>のインスタンスに代入することが可能なのですわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかunique_ptr<>では代入などが禁止されていたのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、試しに代入してみましょう

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::shared_ptr<int> a(new int(10));
  std::shared_ptr<int> b = a;
  std::shared_ptr<int> c;
  c = a;
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} コピーコンストラクタや代入演算子にたいして、問題なくコンパイルできるのだ

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

参照カウンタ

\footnotesize \textcolor{lime}{ずんだもん:} ところで、保持しているメモリ領域は、いつ開放されるのだ?

\footnotesize \textcolor{pink}{四国めたん:} 同じメモリ領域を保持しているshared_ptr<>のインスタンスが全てなくなった時ですわね

\footnotesize \textcolor{lime}{ずんだもん:} インスタンスが全てなくなった時?

\footnotesize \textcolor{pink}{四国めたん:} はい、例えば、コピーコンストラクタや代入演算子により、shared_ptr<>のインスタンス"a", "b", "c"は、いづれも同じメモリ領域を所有することになりますわ

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

\footnotesize \textcolor{pink}{四国めたん:} この時、コピーコンストラクタや代入演算子により、管理するshared_ptr<>のインスタンスの数を 参照カウンタ という特別なカウンタで管理していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} はい、この 参照カウンタ が0になると、管理しているメモリ領域を削除しますわ

\footnotesize \textcolor{lime}{ずんだもん:} つまり、メイン関数を抜ける際に、shared_ptr<>のインスタンス"a", "b", "c"の全てが削除されると、int型のメモリ領域が自動的に削除されると云うことか

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

shared_ptr<>はメモリ領域を1つしか保持できません

\footnotesize \textcolor{pink}{四国めたん:} ところでshared_ptr<>が管理できるメモリ領域は1つまでという制限がありますわ

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

\footnotesize \textcolor{pink}{四国めたん:} ですので、既に生成されているshared_ptr<>のインスタンスに、別のメモリ領域を割り当てるには、resetメソッドが使えますわ

std::shared_ptr::reset(メモリ領域へのポインタ)

\footnotesize \textcolor{pink}{四国めたん:} そしてresetメソッドにより新しくメモリ領域を割り当てられると、保持していた古いメモリ領域にはアクセスできなくなりますわ

\footnotesize \textcolor{lime}{ずんだもん:} その辺りもunique_ptr<>と似ているのだ

\footnotesize \textcolor{pink}{四国めたん:} その際、 参照カウンタ が1だけ減りますわ

\footnotesize \textcolor{lime}{ずんだもん:} 結果として 参照カウンタ が0になったら、メモリ領域が開放されるのだ

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

\footnotesize \textcolor{pink}{四国めたん:} なお、メモリ領域へのポインタにnullptrをセットするか、何も指定しない場合には、単に現在保持しているメモリ領域へのアクセスができなくなりますわ

\footnotesize \textcolor{lime}{ずんだもん:} 参照カウンタ も減るのだ

\footnotesize \textcolor{pink}{四国めたん:} ちなみに、新しく割り当てるメモリ領域の型は、shared_ptr<>のテンプレート引数と同じでなければなりませんわ

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえず簡単なサンプルコードを見てみましょう。

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::shared_ptr<int> a(new int(10));
  int* pb = new int(5);
  a.reset(pb);
  std::cout << *a << std::endl;
  return 0;
}

内容の入れ替え

\footnotesize \textcolor{pink}{四国めたん:} まず、10で初期化したint型のメモリ領域を初期値としてshared_ptr<int>のインスタンス"a"を生成していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} 次に、5で初期化したint型のメモリ領域を作成し、resetメソッドで"a"に管理を委ねますわ

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

\footnotesize \textcolor{pink}{四国めたん:} この時点で、最初に"a"で管理していた、10で初期化したint型のメモリ領域へのアクセスの権利はなくなりますわ

\footnotesize \textcolor{lime}{ずんだもん:} つまり、shared_ptr<int>のインスタンスは、int型のメモリ領域を1つだけしか保持できないのだ

\footnotesize \textcolor{pink}{四国めたん:} ちなみに、10で初期化したint型のメモリ領域を管理するshared_ptr<>のインスタンスがなくなったので、そのメモリ領域は自動的に開放されますわ

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

\footnotesize \textcolor{pink}{四国めたん:} なお、shared_ptr<>のインスタンスに間接演算子""を適用すると、保持しているメモリ領域に間接演算子""を適用するのと同じ効果が得られますわ

\footnotesize \textcolor{lime}{ずんだもん:} サンプルコードの*aは、int型の整数の5を返すと云うことか?

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

\footnotesize \textcolor{pink}{四国めたん:} 最後に、メイン関数を抜ける際に、"a"に保持されたメモリ領域"pb"は、"a"によって暗黙的にdeleteされますわ

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

メモリ領域の移動 move関数を使いましょう

\footnotesize \textcolor{lime}{ずんだもん:} ところでshared_ptr<>にはmove関数はあるのか?

\footnotesize \textcolor{pink}{四国めたん:} いいえ、shared_ptr<>はコピーが許されているので、unique_ptr<>moveに相当する関数はありませんわ

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

\footnotesize \textcolor{pink}{四国めたん:} ただ、moveメソッドにより、unique_ptr<>のインスタンスで保持しているメモリ領域をshared_ptr<>のインスタンスに移すことは可能ですわ

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえずいくつかの例をあげてみますわ

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::unique_ptr<int> a0(new int(10));
  std::unique_ptr<int> a1(new int(10));
  std::unique_ptr<int> a2(new int(10));
  std::shared_ptr<int> b(std::move(a0));
  std::shared_ptr<int> c = std::move(a1);
  std::shared_ptr<int> d;
  d = std::move(a2);
  return 0;
}

\footnotesize \textcolor{pink}{四国めたん:} まず、unique_ptr<>のインスタンス"a0", "a1", "a2"には10で初期化されたint型のメモリ領域を初期値として指定していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} 次に、shared_ptr<>のインスタンス"b"は"a0"を引数としたmove関数を初期値として指定していますわ

\footnotesize \textcolor{lime}{ずんだもん:} shared_ptr<>のコンストラクタの引数にunique_ptr<>のインスタンスをそのまま使うことはできないのだ

\footnotesize \textcolor{pink}{四国めたん:} これでint型のメモリ領域の管理、つまり所有権が"a0"から"b"に移りますわ

\footnotesize \textcolor{lime}{ずんだもん:} 当然、"a0"が管理するメモリ領域はなくなるのだ

\footnotesize \textcolor{pink}{四国めたん:} 次に"c"は"a1"を引数としたmove関数の戻り値をコピーコンストラクタでコピーしていますわ

\footnotesize \textcolor{lime}{ずんだもん:} shared_ptr<>のコピーコンストラクタの引数にunique_ptr<>のインスタンスをそのまま使うことはできないのだ

\footnotesize \textcolor{pink}{四国めたん:} これでint型のメモリ領域の管理、つまり所有権が"a1"から"c"に移りますわ

\footnotesize \textcolor{lime}{ずんだもん:} 当然、"a1"が管理するメモリ領域はなくなるのだ

\footnotesize \textcolor{pink}{四国めたん:} 最後に、"d"は"a2"を引数としたmove関数の戻り値を代入演算子で代入していますわ

\footnotesize \textcolor{lime}{ずんだもん:} shared_ptr<>の代入演算子の引数にunique_ptr<>のインスタンスをそのまま使うことはできないのだ

\footnotesize \textcolor{pink}{四国めたん:} これでint型のメモリ領域の管理、つまり所有権が"a2"から"d"に移りますわ

関数の戻り値としてのshared_ptr<>

\footnotesize \textcolor{lime}{ずんだもん:} ところで、同じ関数内でメモリ領域の所有権を共有してメリットはあるのか?

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

\footnotesize \textcolor{lime}{ずんだもん:} えぇ~

\footnotesize \textcolor{pink}{四国めたん:} 所有権をshared_ptr<>の別のインスタンスと共有するくらいなら、そのまま元のインスタンスを使い続ければいいのですから...

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

\footnotesize \textcolor{pink}{四国めたん:} shared_ptr<>のメモリ領域の所有権の共有は、関数の引数や戻り値に使う場合に便利ですわ

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえず実例を見てみましょう

#include <iostream>
#include <memory>

const int kSize = 50;

std::shared_ptr<char[]> SetMessage(std::shared_ptr<char[]> text) {
  sprintf_s(text.get(), kSize, "これはテストです。\n");
  return text;
}

int main(int argc, char* argv[]) {
  std::shared_ptr<char[]> str(new char[kSize]);
  std::shared_ptr<char[]> message = SetMessage(str);
  std::cout << message.get() << std::endl;
  return 0;
}

関数の中のshared_ptr

\footnotesize \textcolor{pink}{四国めたん:} まずSetMessage関数の引数にshared_ptr<char[]>を指定していますわ

\footnotesize \textcolor{lime}{ずんだもん:} この引数はクラスのインスタンスなので、SetMessage関数の実引数のコピーとなるのだ

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

\footnotesize \textcolor{pink}{四国めたん:} なお、unique_ptr<>と異なり、shared_ptr<>のインスタンスのコピーは可能なので、実引数にはshared_ptr<>のインスタンスをそのまま使用していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} 当然ですが、元のインスタンスの"str"と引数はメモリ領域の所有権を共有していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} 次にSetMessage関数の戻り値の型もshared_ptr<>としていますわ

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえず戻り値もコピーとなるので、メモリ領域の所有権を共有していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} そして、SetMessage関数から戻ると、引数にコピーされたshared_ptr<>のインスタンスは破棄されますわ

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

\footnotesize \textcolor{pink}{四国めたん:} その際、メモリ領域を所有しているインスタンスは1つ減りますが、元の"str"や新たにコピーされた"message"が所有権を維持しているため、メモリ領域は削除されずに残りますわ

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

メソッドでshared_ptr<>のメンバ変数を返す

\footnotesize \textcolor{lime}{ずんだもん:} ところで、クラスでshared_ptr<>として定義されたメンバ変数をメソッドで返すことも可能なのか?

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

\footnotesize \textcolor{pink}{四国めたん:} 前回ではCircleクラスのMessageメソッドでは、メンバ変数"pmessage_"が保持するメモリ領域をgetメソッドを使って返していましたわ

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

\footnotesize \textcolor{pink}{四国めたん:} 今回は、そのままコピーを返してメモリ領域の所有権を共有してみましょう

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 std::shared_ptr<char[]> Message() 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_;
  }
};

#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[2]);
  c[1].Diameter(5.0);
  std::cout << c[0].Message().get() << std::endl;
  std::cout << c[1].Message().get() << std::endl;
  return 0;
}

メソッドの戻り値

\footnotesize \textcolor{lime}{ずんだもん:} CircleクラスのMessageメソッドの戻り値の型がstd::shared_ptr<char[]>となっているのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、戻り値は"pmessage_"をそのまま返していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} この時点で"pmessage_"が管理しているchar型の配列の所有権は共有されますわ

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

\footnotesize \textcolor{pink}{四国めたん:} なお、"pmessage_"の内容が変わるので、Messageメソッドのconst修飾子は削除していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} ちなみにunique_ptr<>とは異なり、Messageメソッドを再び呼び出しても、"pmessage_"はchar型の配列を保持しているため正常に動作しますわ

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

メモリ領域の所有権チェック

\footnotesize \textcolor{lime}{ずんだもん:} ところで、shared_ptr<>のインスタンスがメモリ領域を確保しているかどうか、確認する方法はあるのか?

\footnotesize \textcolor{pink}{四国めたん:} 条件式としてshared_ptr<>のインスタンス名を使うと、bool型の値が返りますわね

\footnotesize \textcolor{lime}{ずんだもん:} unique_ptr<>と同じなのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、例えばこんな感じですわ

std::shared_ptr<int> a(new int(0));
    :
if (a) {
    :
}

\footnotesize \textcolor{lime}{ずんだもん:} if文の条件式としてshared_ptr<int>のインスタンス"a"をそのまま指定しているのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、"a"がint型のメモリ領域を保持している場合は、trueが返りますわ

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

make_shared<>関数を使いましょう

\footnotesize \textcolor{lime}{ずんだもん:} ところで、shared_ptr<>に渡すメモリ領域を生成するのに、明示的にnewを使っていることに違和感があるのだが...

\footnotesize \textcolor{pink}{四国めたん:} どのあたりですか?

\footnotesize \textcolor{lime}{ずんだもん:} deleteを明示的に使っていないのだから、newも使わないで済ませたいのだ

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

\footnotesize \textcolor{pink}{四国めたん:} 実はshared_ptr<>のインスタンスを生成するmake_shared<>と云う関数がありますわ

\footnotesize \textcolor{lime}{ずんだもん:} make_unique<>関数のような感じなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、make_unique<>関数より前のC++11から使えるので、Visual Studio 2022であれば問題ないですわね

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

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

std::make_shared<クラス名>(初期化値)

\footnotesize \textcolor{pink}{四国めたん:} テンプレート引数にはshared_ptr<>が管理するメモリ領域の型名を指定しますわ

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

\footnotesize \textcolor{pink}{四国めたん:} また、引数にはクラスのコンストラクタに与える引数を指定しますわ

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

\footnotesize \textcolor{pink}{四国めたん:} そして、make_shared<>関数の戻り値は、そのままshared_ptr<>の初期化や代入に使用できますわ

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

\footnotesize \textcolor{pink}{四国めたん:} また、インスタンスの宣言時にautoを使えるのも便利ですわね

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえず実例を見てみましょう

#include <iostream>
#include <memory>

#include "circle.h"

int main(int argc, char* argv[]) {
  auto a(std::make_shared<int>(10));        // 初期化値
  auto b = std::make_shared<Circle>(10.0);  // コピーコンストラクタ
  std::shared_ptr<int> c;
  c = std::make_shared<int>(0);  // 代入
  return 0;
}

\footnotesize \textcolor{pink}{四国めたん:} なお、配列の場合はこのようになりますが、使えるのはC++20以降ですわ

std::make_shared<クラス名[]>(要素数)

\footnotesize \textcolor{lime}{ずんだもん:} 前回、 C++言語標準ISO C++20 標準 に変えているので問題ないのだ

\footnotesize \textcolor{pink}{四国めたん:} いづれにしても、わざわざnewを使ってメモリ領域を確保してからshared_ptr<>に割り当てるのは面倒ですわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかに...

\footnotesize \textcolor{pink}{四国めたん:} 関数呼び出しだけで済んでしまうmake_shared<>の方が使い勝手が良いと思うので、積極的に使いましょう

その他のメソッド

shared_ptr<>クラスには様々なメソッドが存在します

今回はswapuse_countについてお話しします

swap

unique_ptr<>の場合と同じく、引数に指定したshared_ptr<>のインスタンスと保持しているメモリ領域の交換をおこないます

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::shared_ptr<int> a(new int(10));
  std::shared_ptr<int> b(new int(5));
  a.swap(b);
  std::cout << "aの値は" << *a << "です。\n" << std::endl;
  std::cout << "bの値は" << *b << "です。\n" << std::endl;
  return 0;
}

まぁ、わざわざメモリ領域を交換する場面は思い浮かばないですが...

ソートをおこなう場合などに使うのでしょうか?

use_count

保持しているメモリ領域が、いくつのshared_ptr<>のインスタンスと共有されているかを返します

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::shared_ptr<int> a(new int(10));
  std::shared_ptr<int> b(a);
  std::cout << "メモリ領域は" << a.use_count() << "で共有されています。\n" << std::endl;
  return 0;
}

まぁ、共有されている数を気にしなければならないアルゴリズムは、検討の余地が大きいと思いますが...

まとめ

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

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

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

\footnotesize \textcolor{lime}{ずんだもん:} ところでunique_ptr<>shared_ptr<>のどちらを使う方がいいのか、迷ってしまうのだ

\footnotesize \textcolor{pink}{四国めたん:} shared_ptr<>の方がコピーできるぶん、便利そうですが、処理に時間がかかり、管理用のメモリも多く使うようですわ

\footnotesize \textcolor{lime}{ずんだもん:} 通常はunique_ptr<>の方がいいのか?

\footnotesize \textcolor{pink}{四国めたん:} まぁ、現状のコンピューターでは、そこまで気にする必要はないので、当面は使いやすいと思う方を使えばいいのでは?

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

\footnotesize \textcolor{lime}{ずんだもん:} ところで次はweak_ptr<>の解説をしてくれるのか?

\footnotesize \textcolor{pink}{四国めたん:} 申し訳ないのですが、weak_ptr<>のお話しは、当面、おこなわない予定ですわ

\footnotesize \textcolor{lime}{ずんだもん:} えぇっ、なぜなのだ?

\footnotesize \textcolor{pink}{四国めたん:} よほど複雑なプログラムではない限り、weak_ptr<>を使う場面がないからですわね

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

Discussion