🖥️

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

に公開

https://youtu.be/fiJ2hHnwWgU

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

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

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

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

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

\footnotesize \textcolor{lime}{ずんだもん:} レッツゴーなのだ

unique_ptr<>は制限があります

\footnotesize \textcolor{pink}{四国めたん:} 前回はunique_ptr<>の概要についてお話ししましたわ

\footnotesize \textcolor{lime}{ずんだもん:} おぼえているのだ

\footnotesize \textcolor{pink}{四国めたん:} 実はunique_ptr<>には制限がありますわ

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

\footnotesize \textcolor{pink}{四国めたん:} はい、主な制限はこの2つですわ

  1. 管理できるメモリ領域は1つまで
  2. コピーができない

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

\footnotesize \textcolor{pink}{四国めたん:} まず1番目ですが、unique_ptr<>が管理できるメモリ領域は1つまでと決まっていますわ

\footnotesize \textcolor{lime}{ずんだもん:} 2つ以上は保持できないということか...

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

\footnotesize \textcolor{lime}{ずんだもん:} unique_ptr<>のインスタンスの宣言、初期化時以外では、どのようにしてメモリ領域を割り当てるのだ?

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} そしてresetメソッドにより新しくメモリ領域を割り当てられると、保持していた古いメモリ領域は自動的に開放されますわ

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

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

\footnotesize \textcolor{lime}{ずんだもん:} つまりresetメソッドで明示的にメモリ領域を開放することができるということか?

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

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

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

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

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::unique_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型のメモリ領域を初期値としてunique_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}{ずんだもん:} つまり、unique_ptr<int>のインスタンスは、int型のメモリ領域を1つだけしか保持できないのだ

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

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

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

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

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

unique_ptr<>はコピーできません

\footnotesize \textcolor{pink}{四国めたん:} 次に2番目ですが、unique_ptr<>のインスタンスを別のunique_ptr<>のインスタンスに代入することはできませんわ

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

\footnotesize \textcolor{pink}{四国めたん:} 実はunique_ptr<>クラスでは、コピーコンストラクタやコピーをするための代入演算子"="が= deleteにより削除定義されていますわ

\footnotesize \textcolor{lime}{ずんだもん:} つまり、使用できないようになっていると云うことか...

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

#include <iostream>
#include <memory>

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

代入でのエラー

\footnotesize \textcolor{pink}{四国めたん:} まず、b = aではコピーコンストラクタが呼ばれますが、 これは削除された関数です というエラーが出ていますわ

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

\footnotesize \textcolor{pink}{四国めたん:} また、c = aはコピーのための代入演算子ですが、こちらについても これは削除された関数です というエラーが出ていますわ

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

\footnotesize \textcolor{pink}{四国めたん:} これは、unique_ptr<>の最大の特徴で、同じメモリ領域を複数のunique_ptr<>のインスタンスで共有することができないのですわ

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

共有してはいけません

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

\footnotesize \textcolor{lime}{ずんだもん:} では、unique_ptr<>のインスタンスで保持されているメモリ領域を別のunique_ptr<>のインスタンスで使いたい場合には、どうすればいいのだ?

\footnotesize \textcolor{pink}{四国めたん:} 実はunique_ptr<>のインスタンスで保持されているメモリ領域を別のunique_ptr<>のインスタンスに移すための関数moveが用意されていますわ

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

\footnotesize \textcolor{pink}{四国めたん:} moveは、このような使い方ができますわ

#include <iostream>
#include <memory>

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

\footnotesize \textcolor{pink}{四国めたん:} まず、"a"には10で初期化されたint型のメモリ領域を初期値として指定していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} 次に、"b"は"a"を引数としたmove関数を初期値として指定していますわ

\footnotesize \textcolor{lime}{ずんだもん:} これでint型のメモリ領域の管理が"a"から"b"に移るのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、メモリ領域の所有権が"a"から"b"に移動することになりますわ

\footnotesize \textcolor{pink}{四国めたん:} 当然ですが、"a"が管理するメモリ領域はなくなりますわ

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

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

\footnotesize \textcolor{lime}{ずんだもん:} これでint型のメモリ領域の管理、つまり所有権は"b"から"c"に移るのだ

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

\footnotesize \textcolor{lime}{ずんだもん:} うむ、これでint型のメモリ領域の管理、つまり所有権は"c"から"d"に移るのだ

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

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

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

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

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

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

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

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

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

#include <iostream>
#include <memory>

const int SIZE = 50;

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

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

関数の中のmove

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} ただ、unique_ptr<>のインスタンスのコピーはできませんので、実引数にはmove関数を使用していますわ

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

\footnotesize \textcolor{pink}{四国めたん:} 当然ですが、元のインスタンスの"str"は、この時点でメモリ領域の所有権を失いますわ

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえず戻り値もコピーとなるので、move関数を使ってリターンしていますわ

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

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

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} 今回はmove関数を使って、メモリ領域の所有権を移動してみましょう

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 std::unique_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 std::move(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[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::unique_ptr<char[]>となっているのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、戻り値は"pmessage_"を引数としたmove関数を使っていますわ

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

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} ちなみにMessageメソッドを再び呼び出すと、"pmessage_"はchar型の配列を保持していないのでエラーの原因となりますわ

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

\footnotesize \textcolor{pink}{四国めたん:} ですので、戻り値としてmove関数を使ってunique_ptr<>の所有権を移動する方法は、お勧めできませんわね

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

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

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

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

\footnotesize \textcolor{lime}{ずんだもん:} どういうことだ?

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

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

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

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

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

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

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} 実はC++14から、unique_ptr<>のインスタンスを生成する関数make_unique<>が追加されましたわ

\footnotesize \textcolor{lime}{ずんだもん:} C++14からならVisual Studio 2022で問題なく使えるのだ

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

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

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} そして、make_unique<>関数の戻り値は、そのままunique_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_unique<int>(10));        // 初期化値
  auto b = std::make_unique<Circle>(10.0);  // コピーコンストラクタ
  std::unique_ptr<int> c;
  c = std::make_unique<int>(0);             // 代入
  return 0;
}

\footnotesize \textcolor{pink}{四国めたん:} なお、配列の場合はこのようになりますわ

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

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

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

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

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

その他のメソッド

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

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

release

getメソッドと同じく、保持しているメモリ領域へのポインタを返します

getと異なるのは、返したメモリ領域の所有権を破棄することです

つまり、返されたメモリ領域は、deleteなどを使って明示的に破棄する必要があります

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::unique_ptr<int> a(new int(10));
  int* pa = a.release();
  delete pa;
  return 0;
}

まぁ、自動で管理されているメモリ領域をわざわざプログラマーが管理しなければならないようにするメリットはあまり無いとは思いますが...

swap

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

#include <iostream>
#include <memory>

int main(int argc, char* argv[]) {
  std::unique_ptr<int> a(new int(10));
  std::unique_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;
}

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

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

まとめ

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

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

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

Discussion