このページでは、オブジェクトをコピーすることについて、これまでのページよりも詳しく解説します。コピーを行う方法として、コピーコンストラクタとコピー代入演算子を取り上げます。これらのメンバ関数を実装する理由の1つであるディープコピーのために、動的オブジェクトの作り方とデストラクタについても取り上げます。
このページの解説は C++14 をベースとしています。
以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。
あるクラス型のオブジェクト a と b があるとき、a = b
のような代入をおこなうと、a の値は b と同じになり、b の値は変化しません。「演算子のオーバーロード」のページで取り上げたとおり、代入演算子をオーバーロードすることによって動作を変更することができますが、通常、コピー (copy) とはこのような動作のことです。
もう1つコピーがおこなわれる場面として、C c2(c1);
のように、コンストラクタにほかのオブジェクトを渡しておこなう初期化があります。つまり、すでに存在しているオブジェクトのコピーをあらたに作るということです。こちらはコピーコンストラクタと呼ばれるコンストラクタによるものです。
コピーコンストラクタと、コピー初期化(「コンストラクタ」のページを参照)は異なるものです。コピー初期化は初期化の構文の1つです。
【C言語プログラマー】memcpy関数などを使ってクラス型のオブジェクトをコピーすることは避けるべきです[1]。このページで解説するように、コピー時におこなう処理が定義されていることがあるため、メモリ上のある範囲をそのまま書き写すだけの方法は不適切です。
代入によって行われるコピーのことを、コピーしていることを明確にするために、コピー代入 (copy assignment) と呼ぶことがあります。
コピー代入は、これまでのページでずっと使ってきた通常の代入のことです。クラス型の場合、「演算子のオーバーロード」のページで取り上げた方法で、代入演算子をオーバーロードすれば、その動作を変更できます。コピー代入を実装した代入演算子は、コピー代入演算子 (copy assignment operator) と呼ばれます。
コピー代入演算子は、以下の条件を満たす場合には、コンパイラが暗黙的に生成します[2]。
ムーブコンストラクタ、ムーブ代入演算子はまだ登場していませんが、名前から想像がつくように、コンストラクタや代入演算子の特別なものです。
【上級】つまり、ムーブが可能であるように実装しているクラスは、デフォルトではコピーが行えません(コピーコンストラクタも生成されません)。ムーブもコピーも可能にするためには、両方ともを明示的に定義しなければなりません。
次のサンプルプログラムは、コピー代入演算子を明示的に定義しています。
#include <iostream>
class DataStore {
public:
explicit DataStore(int v) : m_value {v}
{}
inline int get_value() const
{
return m_value;
}
// コピー代入演算子
inline DataStore& operator=(const DataStore& src)
{
m_value = src.m_value;
return *this;
}
private:
int m_value;
};
int main()
{
{100};
DataStore ds1 {200};
DataStore ds2 {300};
DataStore ds3
= ds1;
ds2 std::cout << (ds1).get_value() << " " << (ds2).get_value() << "\n";
= ds2 = ds3;
ds1 std::cout << (ds1).get_value() << " " << (ds2).get_value() << " " << (ds3).get_value() << "\n";
}
実行結果:
100 100
300 300 300
コピー代入演算子の演算子関数は、const参照型の仮引数と、const でない参照型の戻り値を持つのが一般的です。
コンパイラが暗黙的に生成するコピーコンストラクタの動作は、コピー元のオブジェクトが持っているすべての静的でないデータメンバをそれぞれ対応する *this のデータメンバへとコピーすることです。このサンプルプログラムではあえてそのとおりのことを明示的に実装していますが、デフォルトの動作で問題がないのなら、コンパイラが暗黙的に生成するコピーコンストラクタに任せたほうがいいです。=default
を使って、コンパイラが生成したものを使うことを明示するのでも構いません。こうしておけば、データメンバが追加されたときに、実装を変更しわすれるなどの事故を防げます。
なお、データメンバに const が付いているものがあるとコピー先で上書きができないので、コピー代入演算子を実装できません。
明示的に実装する場合は、コピー元とコピー先が同じになるケース(a = a
)でも問題がないことを確認しましょう。
データメンバがポインタの場合、デフォルトのコピーの実装では、コピー元とコピー先のポインタが同じメモリアドレスを持つことになります。このようなコピーの方法は、シャローコピー(浅いコピー) (shallow copy) と呼ばれます。シャローコピーが正しい場合もありますが、本当にコピーするべきなのは、指し示す先にあるもののほうなのかもしれません。この考え方でおこなうコピーは、ディープコピー(深いコピー) (deep copy) と呼ばれます。コピーの動作を変更する理由の1つとしてよくあるのは、ディープコピーを実装するためです。この話題はあとで改めて取り上げます。
明示的にコピー代入演算子をオーバーロードしたのならば、コピーコンストラクタとデストラクタもあわせて定義することを検討するべきです[3](コピーコンストラクタとデストラクタはあとで取り上げます)。デフォルトで生成されるコピー代入演算子の実装で都合が悪い理由があるというのなら、もう1つのコピー方法であるコピーコンストラクタの実装も変更しなければならないはずです。また、コピーの動作を変更したのなら、おそらく終了処理にも変更の必要性があるため、デストラクタの定義も検討されるべきです。
コンストラクタを以下のルールで宣言した場合、すでに存在しているオブジェクトからコピーを作成するときに使うコピーコンストラクタ (copy constructor)であると認識されます[4]。
コピーコンストラクタは、以下の条件を満たす場合には、コンパイラが暗黙的に生成します[5]。
【上級】つまり、ムーブが可能であるように実装しているクラスは、デフォルトではコピーが行えなくなります。ムーブもコピーも可能にするためには、両方ともを明示的に定義しなければなりません
暗黙的に生成されたコピーコンストラクタは、すべての静的でないデータメンバをコピーするように実装されます。
次のサンプルプログラムはコピーコンストラクタを明示的に定義しています。
#include <iostream>
class DataStore {
public:
// コンストラクタ
explicit DataStore(int v) : m_value {v}
{}
// コピーコンストラクタ
(const DataStore& other) : m_value {other.m_value}
DataStore{}
// コピー代入演算子
inline DataStore& operator=(const DataStore& src)
{
m_value = src.m_value;
return *this;
}
inline int get_value() const
{
return m_value;
}
private:
int m_value;
};
int main()
{
{100};
DataStore ds1 {ds1}; // コピーコンストラクタを使う
DataStore ds2
std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";
}
実行結果:
100 100
コンストラクタ初期化子(「コンストラクタ」のページを参照)が使えることに違いはありますが、コピー代入演算子の場合と同様、通常はすべての静的でないデータメンバをコピーするように実装します。デフォルトの動作で構わないのなら、コンパイラが暗黙的に生成するコピーコンストラクタに任せたほうがいいです。=default
を使って、コンパイラが生成したものを使うことを明示しても構いません。こうしておけば、データメンバが追加されたときに、実装を変更しわすれるなどの事故を防げます。
コピー代入と大きく異なるのは、コピーコンストラクタはこれから新しいオブジェクトを作ろうとしているのだという点です。データメンバはこれから初期化されるところなので、以前の値を上書きすることに関する考慮は不要です。また、自分自身がコピー元となることもありません。
コピーコンストラクタを明示的に定義したのならば、コピー代入演算子と、このあと説明するデストラクタもあわせて定義することを検討するべきです[3]。
RingBufferクラステンプレートにもコピーコンストラクタを定義しておきます。といってもデフォルトの動作で問題ないので、=default
を使うことにします。
namespace mylib {
template <typename T, std::size_t Size>
class RingBuffer {
public:
// コピーコンストラクタ
(const RingBuffer& other) = default;
RingBuffer
// ...
}
}
// ring_buffer.h
#ifndef RING_BUFFER_H_INCLUDED
#define RING_BUFFER_H_INCLUDED
#include <algorithm>
#include <array>
#include <cassert>
namespace mylib {
// リングバッファ
template <typename T, std::size_t Size>
class RingBuffer {
template <typename T, std::size_t Size>
friend class RingBuffer;
public:
// イテレータ
class Iterator {
public:
using value_type = T; // 要素型
using reference = value_type&; // 要素の参照型
using const_reference = const value_type&; // 要素の参照型
using pointer = value_type*; // 要素のポインタ型
using const_pointer = const value_type*; // 要素の constポインタ型
using size_type = std::size_t; // サイズ型
using difference_type = std::ptrdiff_t; // 距離型
public:
// コンストラクタ
(RingBuffer& body, size_type pos, bool is_past_the_end);
Iterator
// ==演算子
inline bool operator==(const Iterator& rhs) const
{
return m_body == rhs.m_body
&& m_pos == rhs.m_pos
&& m_is_past_the_end == rhs.m_is_past_the_end;
}
// !=演算子
inline bool operator!=(const Iterator& rhs) const
{
return !(*this == rhs);
}
// *演算子(間接参照)
inline reference operator*()
{
return *common_get_elem_ptr(this);
}
// *演算子(間接参照)
inline const_reference operator*() const
{
return *common_get_elem_ptr(this);
}
// ->演算子
inline pointer operator->()
{
return common_get_elem_ptr(this);
}
// ->演算子
inline const_pointer operator->() const
{
return common_get_elem_ptr(this);
}
// ++演算子(前置)
& operator++();
Iterator
// ++演算子(後置)
operator++(int);
Iterator
private:
template <typename T>
inline static auto* common_get_elem_ptr(T* self)
{
return &self->m_body.m_data[self->m_pos];
}
private:
& m_body; // 本体のリングバッファへの参照
RingBuffersize_type m_pos; // 指し示している要素の位置
bool m_is_past_the_end; // 終端の次を指すイテレータか
};
// constイテレータ
class ConstIterator {
public:
using value_type = T; // 要素型
using const_reference = const value_type&; // 要素の参照型
using const_pointer = const value_type*; // 要素の constポインタ型
using size_type = std::size_t; // サイズ型
using difference_type = std::ptrdiff_t; // 距離型
public:
// コンストラクタ
(const RingBuffer& body, size_type pos, bool is_past_the_end);
ConstIterator
// ==演算子
inline bool operator==(const ConstIterator& rhs) const
{
return m_body == rhs.m_body
&& m_pos == rhs.m_pos
&& m_is_past_the_end == rhs.m_is_past_the_end;
}
// !=演算子
inline bool operator!=(const ConstIterator& rhs) const
{
return !(*this == rhs);
}
// *演算子(間接参照)
inline const_reference operator*() const
{
return *common_get_elem_ptr(this);
}
// ->演算子
inline const_pointer operator->() const
{
return common_get_elem_ptr(this);
}
// ++演算子(前置)
& operator++();
ConstIterator
// ++演算子(後置)
operator++(int);
ConstIterator
private:
template <typename T>
inline static auto* common_get_elem_ptr(T* self)
{
return &self->m_body.m_data[self->m_pos];
}
private:
const RingBuffer& m_body; // 本体のリングバッファへの参照
size_type m_pos; // 指し示している要素の位置
bool m_is_past_the_end; // 終端の次を指すイテレータか
};
public:
using container_type = typename std::array<T, Size>; // 内部コンテナの型
using value_type = typename container_type::value_type; // 要素型
using reference = typename container_type::reference; // 要素の参照型
using const_reference = typename container_type::const_reference; // 要素の const参照型
using pointer = typename container_type::pointer; // 要素のポインタ型
using const_pointer = typename container_type::const_pointer; // 要素の constポインタ型
using size_type = typename container_type::size_type; // サイズ型
using iterator = Iterator; // イテレータ型
using const_iterator = ConstIterator; // constイテレータ型
public:
// コンストラクタ
() = default;
RingBuffer
// コピーコンストラクタ
(const RingBuffer& other) = default;
RingBuffer
// テンプレート変換コンストラクタ
template <typename U, std::size_t Size2>
(const RingBuffer<U, Size2>& other);
RingBuffer
// コピー代入演算子
& operator=(const RingBuffer& rhs) = default;
RingBuffer
// ==演算子
bool operator==(const RingBuffer& rhs) const;
// !=演算子
inline bool operator!=(const RingBuffer& rhs) const
{
return !(*this == rhs);
}
// 要素を追加
void push_back(const value_type& value);
// 要素を取り除く
void pop_front();
// 空にする
void clear();
// 先頭の要素の参照を返す
inline reference front()
{
return common_front(this);
}
// 先頭の要素の参照を返す
inline const_reference front() const
{
return common_front(this);
}
// 末尾の要素の参照を返す
inline reference back()
{
return common_back(this);
}
// 末尾の要素の参照を返す
inline const_reference back() const
{
return common_back(this);
}
// 先頭の要素を指す イテレータを返す
inline iterator begin()
{
return iterator(*this, m_front, empty());
}
// 末尾の要素の次を指す イテレータを返す
inline iterator end()
{
return iterator(*this, m_back, true);
}
// 先頭の要素を指す constイテレータを返す
inline const_iterator begin() const
{
return const_iterator(*this, m_front, empty());
}
// 末尾の要素の次を指す constイテレータを返す
inline const_iterator end() const
{
return const_iterator(*this, m_back, true);
}
// 容量を返す
inline size_type capacity() const
{
return m_data.size();
}
// 要素数を返す
inline size_type size() const
{
return m_size;
}
// 空かどうかを返す
inline bool empty() const
{
return m_size == 0;
}
// 満杯かどうかを返す
inline bool full() const
{
return m_size == capacity();
}
private:
// 次の位置を返す
inline size_type get_next_pos(size_type pos) const
{
return (pos + 1) % capacity();
}
// 手前の位置を返す
inline size_type get_prev_pos(size_type pos) const
{
if (pos >= 1) {
return pos - 1;
}
else {
return m_size - 1;
}
}
template <typename T>
inline static auto& common_front(T* self)
{
assert(self->empty() == false);
return self->m_data[self->m_front];
}
template <typename T>
inline static auto& common_back(T* self)
{
assert(self->empty() == false);
return self->m_data[self->get_prev_pos(self->m_back)];
}
private:
container_type m_data{}; // 要素
size_type m_size{0}; // 有効な要素の個数
size_type m_back{0}; // 次に push される位置
size_type m_front{0}; // 次に pop される位置
};
// コンストラクタ(異なる要素型の RingBuffer から作成)
template <typename T, std::size_t Size>
template <typename U, std::size_t Size2>
<T, Size>::RingBuffer(const RingBuffer<U, Size2>& other) :
RingBufferm_data(other.m_data.capacity()),
m_size {other.m_size},
m_back {other.m_back},
m_front {other.m_front}
{
std::transform(
std::cbegin(other.m_data),
std::cend(other.m_data),
std::begin(m_data),
[](const auto& e) {
return static_cast<T>(e);
}
);
}
// ==演算子
template <typename T, std::size_t Size>
bool RingBuffer<T, Size>::operator==(const RingBuffer& rhs) const
{
return m_data == rhs.m_data
&& m_size == rhs.m_size
&& m_back == rhs.m_back
&& m_front == rhs.m_front;
}
// 要素を追加
template <typename T, std::size_t Size>
void RingBuffer<T, Size>::push_back(const value_type& value)
{
if (full()) {
m_front = get_next_pos(m_front);
}
else {
m_size++;
}
m_data[m_back] = value;
m_back = get_next_pos(m_back);
}
// 要素を取り除く
template <typename T, std::size_t Size>
void RingBuffer<T, Size>::pop_front()
{
assert(empty() == false);
m_front = get_next_pos(m_front);
--m_size;
}
// 空にする
template <typename T, std::size_t Size>
void RingBuffer<T, Size>::clear()
{
m_size = 0;
m_back = 0;
m_front = 0;
}
// ------------ Iterator ------------
// コンストラクタ
template <typename T, std::size_t Size>
<T, Size>::Iterator::Iterator(RingBuffer& body, size_type pos, bool is_past_the_end) :
RingBufferm_body {body},
m_pos {pos},
m_is_past_the_end {is_past_the_end}
{
}
// ++演算子(前置)
template <typename T, std::size_t Size>
typename RingBuffer<T, Size>::Iterator& RingBuffer<T, Size>::Iterator::operator++()
{
assert(!m_is_past_the_end);
m_pos = m_body.get_next_pos(m_pos);
// 終端要素の位置を越えた?
if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
m_is_past_the_end = true;
}
return *this;
}
// ++演算子(後置)
template <typename T, std::size_t Size>
typename RingBuffer<T, Size>::Iterator RingBuffer<T, Size>::Iterator::operator++(int)
{
{*this};
Iterator tmp ++(*this);
return tmp;
}
// ------------ ConstIterator ------------
// コンストラクタ
template <typename T, std::size_t Size>
<T, Size>::ConstIterator::ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end) :
RingBufferm_body {body},
m_pos {pos},
m_is_past_the_end {is_past_the_end}
{
}
// ++演算子(前置)
template <typename T, std::size_t Size>
typename RingBuffer<T, Size>::ConstIterator& RingBuffer<T, Size>::ConstIterator::operator++()
{
assert(!m_is_past_the_end);
m_pos = m_body.get_next_pos(m_pos);
// 終端要素の位置を越えた?
if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
m_is_past_the_end = true;
}
return *this;
}
// ++演算子(後置)
template <typename T, std::size_t Size>
typename RingBuffer<T, Size>::ConstIterator RingBuffer<T, Size>::ConstIterator::operator++(int)
{
{*this};
Iterator tmp ++(*this);
return tmp;
}
}
#endif
ここまでに何度か言葉が登場したデストラクタ📘 (destructor) は、特殊なメンバ関数の1つで、クラス型のオブジェクトが破棄されるときに自動的に呼び出されます。
【上級】ごくわずかな場面では、デストラクタを明示的に呼び出すことがありますが、ほとんどの場面ではこのようなことをする必要はありませんし、してはいけません。
デストラクタは次のように宣言します。
class クラス名 {
~クラス名();
};
名前はクラス名の頭に「~」を付けたものにしなければならず、戻り値型の指定はなく、仮引数もありません。また、デストラクタはクラスに1つだけしか宣言できません。
inlineキーワードおよび virtualキーワードを付加することができます。
【上級】デストラクタに例外指定を明示しなかった場合、暗黙的に noexcept になります。
定義については通常のメンバ関数と同様、クラスの内側にでも外側にでも記述できます。
class クラス名 {
~クラス名()
{
}
};
::~クラス名()
クラス名{
}
アクセス指定子📘の影響を受けるので、これまでのページのようなクラスでは普通、public にします。
【上級】継承の機能を使うと、デストラクタを protected とする意味が生まれます。
デストラクタを constメンバ関数にすることはできませんが、const修飾されたオブジェクトであっても呼び出されます。const修飾されたオブジェクトに対して呼び出されたとしても、デストラクタ内での thisポインタが constポインタになることはありません。
デストラクタを明示的に定義しなければ、コンパイラが暗黙的に生成します。暗黙的なデストラクタはインライン関数であり、public です。
デストラクタを定義したのならば、コピー代入演算子とコピーコンストラクタもあわせて定義することを検討するべきです[3]。
デストラクタは、確実におこなう必要がある終了処理を記述する場所として使用します。要素数に応じた適切なメモリ確保を行っている std::vector や std::string が、最終的にきちんとメモリを返却できるのも、これらのクラステンプレートのデストラクタの働きによるものです。デストラクタを使用する例は、あとで取り上げます。
クラスの静的でないデータメンバがクラス型の場合、それぞれのデストラクタも呼び出される必要がありますが、このときの順序は、コンストラクタが呼び出された順番と逆の順番と定められています。[6]
「コピー代入」のところで触れたように、ポインタをコピーする際の考え方には、シャローコピー(浅いコピー) (shallow copy) と ディープコピー(深いコピー) (deep copy) があります。
シャローコピーは、ポインタの値をそのままコピーするものです。つまり、コピー元のポインタとコピー先のポインタはいずれも同じ場所を指し示すことになります。コンパイラが暗黙的に生成するコピー代入演算子やコピーコンストラクタの実装はシャローコピーです。
#include <iostream>
class MyValue {
public:
explicit MyValue(int* pvalue) : m_pvalue {pvalue}
{}
inline int& operator*()
{
return *m_pvalue;
}
inline const int& operator*() const
{
return *m_pvalue;
}
private:
int* m_pvalue;
};
int main()
{
int v {100};
{&v};
MyValue vp1 {vp1}; // コピー
MyValue vp2 std::cout << *vp1 << " " << *vp2 << "\n";
= 300;
v std::cout << *vp1 << " " << *vp2 << "\n";
}
実行結果:
100 100
300 300
vp2 は vp1 をコピーして作られています。ユーザー定義のコピーコンストラクタはないので、コンパイラが暗黙的に生成したコピーコンストラクタが呼び出されており、MyValue の m_pvalueメンバはシャローコピーされます。vp1 の m_pvalueメンバは、v を指しているので、v の値を変更してみると、vp1 からみた値も vp2 からみた値も同じように変化しています。これは言い換えると、MyValue は指し示す先にあるオブジェクトを所有していない(占有していない)と表現できます。
ディープコピーは、ポインタが指し示す先にあるものをコピーし、ポインタは新しいコピーを指し示すように変更します。この方法では、指し示す先にあるオブジェクトは別物になるので、オブジェクトを所有している(占有している)と表現できます。
ディープコピーを実現する方法として、以下の2つがあります。
=delete
で削除する1番の方法は普通のコピーの構文が使えるため、使う側としては自然ですが、一般的なシャローコピーではないことが伝わりづらいデメリットがあります。
2番の方法は、使用者に意識的にディープコピー用の関数を呼ばせるため、誤解を与える可能性がなくなります。一方で、その関数の戻り値はクラス型のポインタとなってしまいます(これは実装してみると分かります)。
ここからは1番の方法での実装を試みてみます。2番の方法は練習問題で取り上げます。
ディープコピーのために、コピー代入演算子やコピーコンストラクタを明示的に実装するには、データメンバとしてはポインタだけを保持しつつも、その指し示す先にあるオブジェクトのコピーを作り出せなければなりません。これを可能にするためには、動的オブジェクトを使います。
プログラムの実行中の任意の時点で、必要な大きさのメモリ領域を確保することを、動的メモリ割り当て(ダイナミックメモリアロケーション)📘 (dynamic memory allocation) といいます。動的メモリ割り当てによって確保されたメモリ領域の上に作られるオブジェクトを、動的オブジェクト (dynamic object) と呼びます。
new演算子 (new operator) を使って記述する new式 (new expression) によって、動的メモリ割り当てと、動的オブジェクトの生成をまとめて行えます。これを簡単に「newする」などと表現することがあります。
new 型名
new 型名 初期化子
「動的オブジェクトを生成する」とはつまり、コンストラクタを呼び出して、適切に初期化をおこなうということです。
【C言語プログラマー】malloc関数などが行うのは動的メモリ割り当てだけでした。動的オブジェクトを生成するためにコンストラクタを呼び出す過程が抜けているため、malloc関数などを使ってクラス型のオブジェクトを作ることは不適切です。
「型名」に動的オブジェクトの型を記述します。生成されたオブジェクトは「初期化子」に記述した内容を使って初期化されます。型がもつデフォルトコンストラクタを使用できるなら「初期化子」の指定を省略できますが、そうでない場合に省略すると、未初期化な状態になることに注意してください。未初期化な状態を避けるためには、いつものように {}
を使って、明示的に初期化しておくといいです。
new式には、配列の確保をおこなうためのバージョンも存在していますが、ここでは取り上げないことにします。
new式を評価した結果、動的オブジェクトの生成に成功すれば、そのオブジェクトを指し示すポインタが得られます。
int* p = new int {123};
「型名」から推論できるので、変数の側の型名は auto にしても構いません。
auto* p = new int {123};
new式が行う処理には、動的メモリ割り当てと、オブジェクトを生成するためのコンストラクタの実行が含まれています。そのため、以下の2つの場面で失敗する可能性をもっています。
この辺りの話題は少し複雑になるのでページを改めて取り上げることにして、今のところは失敗しない前提で話を進めます。
動的オブジェクトは、動的ストレージ期間📘 (dynamic storage duration) を持ちます。動的ストレージ期間を持つオブジェクトの寿命は、new式によって生成されたときに始まり、delete式 (delete expression) が実行されたところで終了します。delete式は、delete演算子 (delete operator) を使って次のように記述される式です。
delete 動的オブジェクト
delete ヌルポインタ
delete演算子に適用できるのは、new式で生成された動的オブジェクトかヌルポインタだけです。前者の場合は、その動的オブジェクトを解放(破棄)します(このときデストラクタがあれば呼び出される)。後者の場合はなにも起こりません。
このように、動的ストレージ期間を持つオブジェクトの寿命を終了させるには、明示的な delete の指示が必要です。これをよく「deleteする」「解放する」などと表現します。
delete されずに、いつまでもメモリを使用しつづけてしまう状態を、メモリリーク📘 (memory leak) と呼び、C++ の典型的なバグの1つとして知られています。
動的ストレージ期間を持つオブジェクトの delete を確実に行うために、デストラクタを利用できます。new式で得られたポインタをクラスのデータメンバとして持っておき、デストラクタに delete式を書けば、delete を意識せずに使用できます。この仕組みは自作する必要はなく、標準ライブラリにある std::unique_ptr を使うことで実現できますが、詳細は次のページで取り上げることにして、今のところは注意して delete を行うようにしておきます。
次のサンプルプログラムは、動的オブジェクトを使ってディープコピーを実装しています。
#include <iostream>
class MyValue {
public:
explicit MyValue(int v);
(const MyValue& other);
MyValue~MyValue();
& operator=(const MyValue& rhs);
MyValue
inline void set(int v)
{
*m_pvalue = v;
}
inline int get() const
{
return *m_pvalue;
}
private:
int* m_pvalue;
};
::MyValue(int v) : m_pvalue {new int {v}}
MyValue{
}
::MyValue(const MyValue& other) : m_pvalue {new int {*other.m_pvalue}}
MyValue{
}
::~MyValue()
MyValue{
delete m_pvalue;
}
& MyValue::operator=(const MyValue& rhs)
MyValue{
if (this != &rhs)
{
// 以前のオブジェクトを解放して、新しいオブジェクトを生成する
delete m_pvalue;
m_pvalue = new int {*rhs.m_pvalue};
}
return *this;
}
int main()
{
(100);
MyValue vp1
{vp1}; // コピー
MyValue vp2 std::cout << vp1.get() << " " << vp2.get() << "\n";
.set(300);
vp1std::cout << vp1.get() << " " << vp2.get() << "\n";
= vp1; // コピー
vp2 std::cout << vp1.get() << " " << vp2.get() << "\n";
.set(500);
vp1std::cout << vp1.get() << " " << vp2.get() << "\n";
}
実行結果:
100 100
300 100
300 300
500 300
vp2 を vp1 にコピーする操作のあとで vp1 のほうを書き換えても、vp2 のほうに影響を与えていないことが分かります。つまりコピー操作後に、ポインタが同じオブジェクトを指していません。
MyValueクラスのデータメンバ m_pvalue はポインタであり、new式で生成した int型の動的オブジェクトを指し示すように管理しています。delete による解放をデストラクタで行っているので、MyValueクラスのオブジェクトが破棄されれば、m_pvalue が指し示す動的オブジェクトも正しく解放されます。
MyValueオブジェクトのコピーコンストラクタでは、コピー元のオブジェクトの m_pvalue の値を初期化子として new式を実行しています。コピーコンストラクタはこれから新しいオブジェクトを作ろうとしているので、m_pvalue を初期化するところで new を使用するだけで済みます。
一方、コピー代入演算子の方は、すでに保持している動的オブジェクトのポインタを解放してやる必要があります。そのあとで new式によって新しい動的オブジェクトを生成します。
コピー代入演算子では、vp1 = vp1
のような自己代入に備えておく必要があります。delete によって vp1.m_pvalue は解放されてしまうため、その直後の new int {*rhs.m_pvalue}
が未定義動作になってしまうためです。自己代入で何も起こらないことが望まれるのなら、this == &rhs
のときには単に return してしまえばいいでしょう。
くわしい解説をまだしていないので深入りしませんが、このサンプルプログラムの実装は new が失敗する可能性を考えていません。もし new が失敗すると、プログラムは強制的に終了されることになります。
保証されたことではないものの、コンパイラが自動的に行っている可能性が高い最適化📘として、RVO (Return Value Optimization。戻り値の最適化) があります。RVO は、関数の戻り値を呼び出し元にコピーすることを避けて、受け取り側のオブジェクトを初期化する最適化です。
struct S {};
()
S f{
return S {}; // S の一時オブジェクトを返す
}
int main()
{
{f()}; // f() が返す S のオブジェクトを受け取って s を初期化する
S s }
実行結果:
関数f にある S {}
という記述は、一時オブジェクト (temporary object) を生成する記法です。この記法は、変数を定義するときの記述から変数名の指定をなくしたものです。つまり名前がない一時的な変数を定義しています。
一時オブジェクトには名前がないので、当然ながら名前を使ったアクセスができません。そういうことをする必要がない場面でのみ用いられるものです。このサンプルプログラムでは、単に、作ったオブジェクトをそのまま返せばいいだけなので、一時オブジェクトで十分です。
一時オブジェクトは、生成された式を含んでいる完全式 (full-expression) が完了したときに自動的に削除され[7]、このときデストラクタが呼び出されます。完全式とは、ほかの式の一部となっていない式のことで、たとえば x = a + b;
の x = a + b
が完全式です(a + b
は x = a + b
の一部になっている式)。
この小さなプログラムでも以下の処理が行われる可能性があります。
しかし、戻り値として返されているものは、return文の中で作られた一時オブジェクトであり、そのまま受け取り側の初期化に使われるだけであるとコンパイラが認識できれば、単に s をデフォルトコンストラクタで生成するだけのコードにまで最適化できる可能性があります。このような最適化が RVO です。
RVO が必ずおこなわれると保証されてはいないものの、一般的な最適化手法であり、実際には行われている可能性が非常に高いです。
【C++17】RVO は正式に保証されることになりました[8]。またそれにより、コピーコンストラクタが使われないことが保証できるため、コピーコンストラクタが定義されていなくても問題なくなりました。
S {}
のような、変数を定義するときの記述から変数名の指定をなくした記述により、名前のない一時オブジェクトを生成できる
新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。
問題の難易度について。
★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。
オブジェクトをコピーすることを禁止するにはどうすればいいですか?
ディープコピーを実装した MyValueクラスのサンプルプログラムを、各種の等価演算子と関係演算子が、ポインタの先にある値による比較になるように実装してください。
ディープコピーを、専用のメンバ関数を作成する方法で実装してください。
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
![]() |
管理者情報 | プライバシーポリシー |