🖥️

【C++言語入門】 第17回 代入

に公開

https://youtu.be/fgk7H2mFua0

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

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

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

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

\footnotesize \textcolor{pink}{四国めたん:} 前回はクラスのインスタンスのコピーについてお話ししましたわ

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

\footnotesize \textcolor{pink}{四国めたん:} 今回はインスタンスの代入についてお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} コピーと代入は違うのか?

\footnotesize \textcolor{pink}{四国めたん:} その辺りも含めてお話ししますわね

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

代入で呼ばれるメソッドは...

\footnotesize \textcolor{pink}{四国めたん:} 前回はインスタンスをコピーすることで新たなインスタンスを 生成 する際に呼ばれるメソッドについてお話ししましたわ

\footnotesize \textcolor{lime}{ずんだもん:} コピーコンストラクタ と呼ばれているのだ

\footnotesize \textcolor{pink}{四国めたん:} ところで、既に生成済みのインスタンスに、別のインスタンスをコピーする場合には、どうなるのでしょうか?

\footnotesize \textcolor{lime}{ずんだもん:} ただ単にメンバがコピーされるのではないか?

\footnotesize \textcolor{pink}{四国めたん:} 確認のために、まず、前回のプログラム例を見てみます。

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

int main(int argc, char* argv[]) {
  Circle c(10.0);
  Circle c0 = c;
  std::cout << "円の直径は" << c0.Diameter() << "cmです。" << std::endl;
  std::cout << c0.Message() << std::endl;
  return 0;
}

\footnotesize \textcolor{lime}{ずんだもん:} Circle c0 = c;という部分で コピーコンストラクタ が呼ばれるのだ

\footnotesize \textcolor{pink}{四国めたん:} 次に、少し変更して実行してみますわ

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

int main(int argc, char* argv[]) {
  Circle c(10.0);
  Circle c0;
  c0 = c;
  std::cout << "円の直径は" << c0.Diameter() << "cmです。" << std::endl;
  std::cout << c0.Message() << std::endl;
  return 0;
}

エラー

\footnotesize \textcolor{lime}{ずんだもん:} ブレークポイント指令が実行されました という警告とともにプログラムが中断したのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、前回 コピーコンストラクタ を追加する前の状況と同じですわね

\footnotesize \textcolor{lime}{ずんだもん:} つまり、既に破棄されたメモリ領域を再度破棄しようとして、プログラムが中断したのか?

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

\footnotesize \textcolor{lime}{ずんだもん:} ただ単に代入によるコピーをクラスのインスタンスの生成と分けただけなのだが...

\footnotesize \textcolor{pink}{四国めたん:} 実は コピーコンストラクタ が呼ばれるのは、クラスのインスタンス生成と同時に既存のインスタンスを代入した場合に限られますわ

\footnotesize \textcolor{lime}{ずんだもん:} 既に生成済みのインスタンスに別のインスタンスを代入した場合には コピーコンストラクタ は呼ばれないのか

\footnotesize \textcolor{pink}{四国めたん:} はい、その場合には、単にメンバ変数などがコピーされるだけですわね

\footnotesize \textcolor{lime}{ずんだもん:} それは結構、問題ではないのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、今回のように不具合が生じる場合がありますわ

\footnotesize \textcolor{pink}{四国めたん:} なので、C++言語では 演算子のオーバーロード を可能とすることにより、不具合の回避が可能となっていますわ

\footnotesize \textcolor{lime}{ずんだもん:} 演算子のオーバーロード

\footnotesize \textcolor{pink}{四国めたん:} はい、四則演算子"+-*/"や代入演算子"="の動作を上書きすることですわ

\footnotesize \textcolor{lime}{ずんだもん:} そんなことができるのか!?

\footnotesize \textcolor{pink}{四国めたん:} 今回は、その中の一つである コピー代入演算子 "="のオーバーロードについて説明しますわ

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

\footnotesize \textcolor{pink}{四国めたん:} メソッドの形式は以下のようになりますわ

クラス名& operator=(const クラス名& 引数名) {
  処理
    :
    :
  return *this;
}

\footnotesize \textcolor{lime}{ずんだもん:} メソッド名以外は普通のメソッドのように見えるのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、メソッド名はoperatorキーワードにオーバーロードする演算子、ここでは"="を付加したものとなりますわ

\footnotesize \textcolor{lime}{ずんだもん:} 引数は代入元のインスタンスを受けるためにクラスの参照型なのだ

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

\footnotesize \textcolor{lime}{ずんだもん:} そして、戻り値はクラスの参照型なのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、returnで自分自身を指す*thisを返していますわね

\footnotesize \textcolor{lime}{ずんだもん:} this

\footnotesize \textcolor{pink}{四国めたん:} はい、thisはインスタンス自身を指すポインタですわ

\footnotesize \textcolor{pink}{四国めたん:} なのでreturn *this;は間接演算子"*"を使ってインスタンス自身を返していますわね

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

\footnotesize \textcolor{pink}{四国めたん:} とりあえずCircleクラスに コピー代入演算子 "="のオーバーロードを追加してみますわ

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

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

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

 public:
  Circle() : Circle(0.0) {}
  Circle(double diameter) : diameter_(diameter) {
    pmessage_ = new char[kMessageSize];
  }
  Circle(const Circle& c) : Circle(c.diameter_) {
    memcpy_s(pmessage_, kMessageSize, c.pmessage_, kMessageSize);
  }
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  };

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

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }

  virtual const char* Message() {
    double area = Area();
    sprintf_s(pmessage_, kMessageSize, "円の面積は%fです。", area);
    return pmessage_;
  }

  virtual Circle& operator=(const Circle& c) {
    diameter_ = c.diameter_;
    memcpy_s(pmessage_, kMessageSize, c.pmessage_, kMessageSize);
    return *this;
  }
};

#endif  // CIRCLE_H

コピー代入演算子

\footnotesize \textcolor{lime}{ずんだもん:} 今度は問題なく実行できたのだ

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

\footnotesize \textcolor{lime}{ずんだもん:} オーバーロードした コピー代入演算子 "="の処理について教えて欲しいのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、処理としては"diameter_"をコピーした後、"pmessage_"のポインタではなくメモリ内容をコピーしていますわ

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

使えなくすれば簡単かも...

\footnotesize \textcolor{pink}{四国めたん:} ところで、 コピーコンストラクタコピー代入演算子 "="も明示的に記述しない場合には、デフォルトの動作としてメンバ変数のコピーのみが行われますわ

\footnotesize \textcolor{lime}{ずんだもん:} その結果、意図しない不具合が生じて、プログラムが止まってしまっていたのだ

\footnotesize \textcolor{pink}{四国めたん:} プログラム例程度のサイズのクラスであれば、これらのメソッドを明示的に記述しても大した手間ではありませんわ

\footnotesize \textcolor{pink}{四国めたん:} でも、サイズの大きいクラスの場合には、結構な手間がかかりますわ

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

\footnotesize \textcolor{pink}{四国めたん:} 使われることが判っている場合には良いのですが、使われる可能性が低い場合には結構面倒ですわ

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

\footnotesize \textcolor{pink}{四国めたん:} いっそのこと、使えなくした方が良いのでは? とか思いませんか?

\footnotesize \textcolor{lime}{ずんだもん:} えぇっ、それでいいのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、C++言語ではそのための手段も提供していますわ

\footnotesize \textcolor{lime}{ずんだもん:} どのようにするのだ?

\footnotesize \textcolor{pink}{四国めたん:} 一番簡単なのは、処理を空にしたメソッドを定義してprivate:指定することでしょうか

private:
  クラス名(const クラス名&) {}
  クラス名& operator=(const クラス名&) { return *this; }

\footnotesize \textcolor{lime}{ずんだもん:} 引数名がないのだが...

\footnotesize \textcolor{pink}{四国めたん:} はい、処理では何もしませんので、引数名の指定は不要ですわ

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

\footnotesize \textcolor{pink}{四国めたん:} ただ、これも同じクラスの中からは使えてしまいますので、完全とは言えませんわ

\footnotesize \textcolor{lime}{ずんだもん:} 完全な方法はないのか?

\footnotesize \textcolor{pink}{四国めたん:} 完全な方法としては 削除済み関数宣言 = deleteを使う方法ですわ

\footnotesize \textcolor{lime}{ずんだもん:} delete

\footnotesize \textcolor{pink}{四国めたん:} はい、deleteと云ってもインスタンスを破棄するための演算子ではなく、関数の削除を示す宣言ですわ

\footnotesize \textcolor{lime}{ずんだもん:} どのようにして使うのだ?

\footnotesize \textcolor{pink}{四国めたん:} 使い方は簡単で、メソッドの宣言の後に付け加えるだけですわ

クラス名(const クラス名&) = delete;
クラス名& operator=(const クラス名&) = delete;

\footnotesize \textcolor{lime}{ずんだもん:} お~、とても簡単なのだ

\footnotesize \textcolor{pink}{四国めたん:} これで コピーコンストラクタコピー代入演算子 は完全に使えなくなりますわ

\footnotesize \textcolor{lime}{ずんだもん:} 試しにCircleクラスで使ってみて欲しいのだ

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

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

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

 public:
  Circle() : Circle(0.0) {}
  Circle(double diameter) : diameter_(diameter) {
    pmessage_ = new char[kMessageSize];
  }
  Circle(const Circle&) = delete;
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  };

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

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }

  virtual const char* Message() {
    double area = Area();
    sprintf_s(pmessage_, kMessageSize, "円の面積は%fです。", area);
    return pmessage_;
  }

  Circle& operator=(const Circle&) = delete;
};

#endif  // CIRCLE_H

代入のエラー

\footnotesize \textcolor{lime}{ずんだもん:} メイン関数内でCircleのインスタンスの代入部分でエラーが出ているのだ

\footnotesize \textcolor{pink}{四国めたん:} これで コピーコンストラクタコピー代入演算子 を使おうとしても、コンパイル時にエラーが出るので判りやすいですわね

default関数宣言

削除済み関数宣言 = deleteの他に、 明示的にデフォルト設定された関数宣言 = defaultというのが存在します

使い方は 削除済み関数宣言 = deleteと同じく、関数の宣言の後に付け加えるだけです

ただ、デフォルトで動作の決まっているメソッドについては、すでに暗黙的に存在しますので、わざわざ明示する必要はありません

唯一、 デフォルトのコンストラクタ については、他の コンストラクタ を作成してしまうと存在しなくなるため、明示的に作成する必要があります

その際、 明示的にデフォルト設定された関数宣言 = defaultを使うことで、 デフォルトのコンストラクタ の動作を簡単に復活することができます

とは云え、 デフォルトの動作 がどのようなものなのか、思った通りに動くのかは非常に不安です

できれば自身でしっかりと動作を規定した方が安心です

まとめ

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

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

\footnotesize \textcolor{pink}{四国めたん:} 以上で 代入 を終わりますわ

Discussion