演算子のオーバーロード | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、演算子のオーバーロードを取り上げます。演算子をオーバーロードすると、オペランドの型に応じて、演算子の動作を変更することが可能になります。ただしルールはやや複雑なうえ、安易に使うとかえって混乱を招いたり、バグの原因になったりする恐れがあるため、よく理解して使うことが重要です。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



オーバーロード演算子 🔗

前のページでは関数のオーバーロード📘を取り上げましたが、演算子もオーバーロードできます(演算子のオーバーロード (operator overload、operator overloading))。

演算子をオーバーロードするといっても、宣言するものは関数です。たとえば、加算をおこなう + という演算子をオーバーロードする場合を考えます。+ という演算子には元々、2つの算術型📘の値を加算する使い方と、配列を指すポインタを使って指し示す要素を移動するアドレス計算としての使い方(「配列とポインタ」のページを参照)があります。

算術型 + 算術型
ポインタ型 + 整数型
整数型 + ポインタ型

これを関数のように表現すると、次のように書けるでしょう。

IntegerType plus(IntegerType lhs, IntegerType rhs);
ArrayType* plus(ArrayType* lhs, IntegerType rhs);
ArrayType* plus(IntegerType lhs, ArrayType* rhs);

lhs と rhs は左辺(左側)と右辺(右側)をあらわすためによく使われる表現です。それぞれ、left-hand side、right-hand side を意味しています。

まさにこのようなかたちで宣言をおこなうことによって演算子をオーバーロードできます。ただし関数名には operator 演算子 という名前を使うルールになっています(あいだのスペースはあってもなくても構いません)。さきほどは仮に plus という名前の関数でコードを書きましたが、実際には operator + という名前の関数を作ればいいということです。このような関数を演算子関数 (operator function) と呼びます。また、オーバーロードされた演算子を、オーバーロード演算子 (overloaded operators) といいます。

plus関数の例からも分かるように、1つの演算子に対して複数の演算子関数を宣言できます。これは通常のオーバーロードと同じ考え方で、仮引数に違いがあれば問題ありません(「オーバーロード」のページを参照)。

また、演算子関数が関数テンプレートであってもよく、演算子関数テンプレート (operator function template) と呼ばれます。ここから先の説明では演算子関数テンプレートも含めて、演算子関数と記述しています。


演算子関数は非メンバ関数か、非静的なメンバ関数でなければなりません(ただし、一方しか許さない演算子もあります)。演算子関数が非メンバ関数の場合は仮引数に、クラス型、クラスの参照型、列挙型、列挙型の参照型のいずれかを含んでいなければなりません。この制約があるのは、int のような標準的な型に対する演算子の挙動を書き換えてしまえると、C++ の基本的なルールが崩壊しかねないためです。

演算子関数の宣言と定義は以下の構文で行います。

// 非メンバ関数の場合
戻り値の型 operator 演算子(仮引数の並び);  // 宣言
戻り値の型 operator 演算子(仮引数の並び)   // 定義
{
}

// 非静的なメンバ関数の場合
class C {
public:
    戻り値の型 operator 演算子(仮引数の並び);  // 宣言
    戻り値の型 operator 演算子(仮引数の並び)   // 定義
    {
    }
};

operator 演算子 の部分が関数名なので、メンバ関数として宣言し、クラス定義の外に定義を書くときは以下のような記述になります。

戻り値の型 C::operator 演算子(仮引数の並び)
{
}

「仮引数の並び」は、対象の演算子のオペランドに応じたものを記述します。たとえば、Moneyクラスに int型の値を加算できるようにするために + 演算子をオーバーロードする場合なら次のようになります。

// 非メンバ関数の場合
Money operator+(const Money& money, int amount);
Money operator+(int amount, const Money& money);

// 非静的なメンバ関数の場合
class Money {
public:
    Money& operator+(int amount);
};

非静的なメンバ関数で実装する場合は、1つ目のオペランドが Money型である場合にだけ演算子関数が使用されます。そのため、money + 100 のような使い方では機能しますが、100 + money では機能しません。一方、非メンバ関数で実装する場合は、仮引数を入れ替えたバージョンを用意すれば、money + 100 でも 100 + money でも機能します。

非静的なメンバ関数による実装には、公開されていないメンバにアクセスしやすい利点があります。ただし、非メンバ関数による実装でも、演算子関数をフレンド関数にする方法で対処できます(「関数テンプレート」のページを参照)。


演算子を使用している箇所から、演算子関数の宣言が見えており、オペランドの型が一致していれば自動的にその演算子関数が使用されます。一応、演算子関数を通常の関数と同じように、x = operator 演算子(実引数の並び); として呼び出すことも可能ですが、通常このようなことをする必要はありません。

#include <iostream>

class Money {
public:
    explicit Money(int amount) : m_amount{amount}
    {}

    inline int get_amount() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

Money operator+(const Money& money, int amount)
{
    return Money {money.get_amount() + amount};
}

int main()
{
    Money m {1000};

    m = m + 500;            // OK. operator+ が呼び出される
    std::cout << m.get_amount() << "\n";

    m = operator+(m, 500);  // OK. わざわざこう書く必要はない
    std::cout << m.get_amount() << "\n";
}

実行結果:

1500
2000

以上のような方法で演算子の動作を変更できます。std::vector が [] で要素にアクセスできたり、std::string を + で連結したり、std::cout が <<、std::cin が >> で入出力を行ったりできているのは、標準ライブラリ側で演算子をオーバーロードしているからです。

オーバーロードできる演算子は以下のものに限られており、sizeof演算子やスコープ解決演算子のように、演算子と呼ばれていてもオーバーロードできないものがあります1

+   -   *   /   %
^   &   |   ~   <<  >>
=   +=  -=  *=  /=  %=  ^=  &=  |=  >>= <<=
==  !=  <   >   <=  >=
!   &&  ||
++  --
,
->  ->*
()
[]
new delete  new[]   delete[]

newdeletenew[]delete[] はここまでに解説していない演算子です。

【C++20】オーバーロードできる演算子に、<=>co_await が追加されています2

+-*& には単項演算子📘と二項演算子📘がそれぞれ存在します。また、++-- には前置と後置がそれぞれ存在します。() は優先順位を変える括弧ではなく、関数呼び出しの括弧です。

演算子関数の本体に何を書くかは自由ですが、使用者が混乱しないように、その演算子のイメージから外れた挙動をしないようにすることは重要です。原則として、int などの基本的な型と同じ挙動になるようにしておくべきです。ただし、文字列の連結に + を用いたり、入力に >>、出力に << を用いたりすることは、標準ライブラリでも行われているので、認められると考えてもいいでしょう。

ここからは演算子の種類ごとに、具体的な方法やルールを取り上げます。

添字演算子 🔗

まずは添字演算子からみていきます。以下の演算子です。

[]

添字演算子は、配列の要素へのアクセスに用いる演算子です。添字演算子をオーバーロードすることで、自作のクラスを配列のように取り扱えるようになります。std::vector や std::string で [] が使えるのは、添字演算子がオーバーロードされているからです。

添字演算子のオーバーロードは必ず非静的なメンバ関数として行い、添字を意味する仮引数を1つだけ持たなければなりません。

【C++23】非静的でなければならない条件が削除されました。operator[] の実装がメンバへのアクセスを必要としていないのなら、静的メンバ関数とすることで thisポインタを渡す分のコストを避けられます。10

a[3] のような使い方をしたとすれば、3 の部分が仮引数に渡されてきます。一般的な添字のイメージからいえば、仮引数は整数型が自然に思えるかもしれませんが、型に制約はありません。

添字に文字列を用いる配列は連想配列📘と呼ばれ、よく使われるデータ構造の一種です。

【C++20】コンマ演算子をオーバーロードすることで、添字を複数指定できる多次元配列のようなクラスを実現できますが、この方法は推奨されなくなりました12

【C++23】多次元配列をサポートできるように、operator [] に複数の引数を持たせられるようになりました。3

以下は使用例です。

#include <iostream>

class DataStore {
public:
    DataStore() : m_values {}
    {}

    inline const int& operator[](std::size_t index) const
    {
        return m_values[index];
    }
    inline int& operator[](std::size_t index)
    {
        return m_values[index];
    }

private:
    int    m_values[100];
};

int main()
{
    DataStore ds {};
    ds[1] = 100;
    std::cout << ds[1] << "\n";
}

実行結果:

100

constメンバ関数版と、非constメンバ関数版を作っています(「オーバーロード」のページを参照)。c[1] = 100 のような書き込みのアクセスをおこなうためには非constメンバ関数版が必要ですし、const なオブジェクトから要素の値を読み取るには、constメンバ関数版が必要です。

戻り値の型は参照型にしますc[1] = 100 という式は、c.operator[](1) = 100 を意味しますから、戻り値に対して代入が行える必要があるためです。

単項演算子 🔗

続いて、以下の単項演算子です。

+   -   *   !   ~   &

+-*& には二項演算子のものも存在しますが、それらは二項演算子のルールに従います。++-- についてはあとで取り上げます

+- は符号を意味する演算子なので、オーバーロードするなら、オブジェクトが数値的なものであるときになります。+ は実質的には効果がない演算子なので、受け取った引数をそのまま return させるだけの実装になるのが普通です。

* は間接参照をおこなう演算子です。オブジェクトがポインタのような存在であるときにオーバーロードし、通常は参照型を返すように実装します。この演算子については、クラスアクセスメンバ演算子のところで取り上げます。

! は論理否定演算子です。オブジェクトの状態を true/false の2択で表現できる場合に、それを反転させる意味でオーバーロードします。通常、戻り値は bool型です。

~ はビット単位NOT の演算子です。オブジェクトにビット単位でアクセスする意義があるときにオーバーロードします。

& はメモリアドレスを取得する演算子です。この演算子をオーバーロードして動作を変えると、メモリアドレスを取得するつもりで使用される事故を起こすので注意が必要です。原則としてオーバーロードしないことを勧めます6

& がオーバーロードされている場合にでもメモリアドレスを取得できるようにするために、std::addressof があります。4

単項演算子のオーバーロードには、非静的なメンバ関数にする方法と、非メンバ関数にする方法の2種類があります。

非静的なメンバ関数による方法 🔗

非静的なメンバ関数にする方法では、仮引数のないメンバ関数を定義します。オブジェクトx に対して演算子を適用すると、x.operator 演算子() のかたちで呼び出されます。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

    inline DataStore operator+() const
    {
        return *this;
    }
    DataStore operator-() const;

private:
    int    m_value;
};

DataStore DataStore::operator-() const
{
    DataStore tmp {-m_value};
    return tmp;
}

int main()
{
    DataStore ds {123};

    std::cout << (+ds).get_value() << "\n"; 
    std::cout << (-ds).get_value() << "\n"; 
}

実行結果:

123
-123

非メンバ関数による方法 🔗

非メンバ関数にする方法では、仮引数を対象のオブジェクトのクラス型や、その参照型にします。オブジェクトx に対して演算子を適用すると、operator 演算子(x) のかたちで呼び出されます。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

private:
    int    m_value;
};

bool operator!(const DataStore& ds)
{
    return ds.get_value() != 0;
}

int main()
{
    DataStore ds1 {123};
    std::cout << std::boolalpha << !ds1 << "\n"; 

    DataStore ds2 {0};
    std::cout << std::boolalpha << !ds2 << "\n"; 
}

実行結果:

true
false

非メンバ関数として実装しようとすると、クラス型の非公開なメンバにアクセスできなくて困ることがあります。そのようなときには、下手に public なメンバ関数を作って穴を開けるのではなく、演算子関数をフレンド関数として指定します(「関数テンプレート」のページを参照)。

インクリメント演算子、デクリメント演算子 🔗

続いて、インクリメント演算子とデクリメント演算子です。

++  --

インクリメント演算子とデクリメント演算子には前置のものと後置のものがあり、オーバーロードは別個に行えます。前置と後置を区別するために、後置の場合にはダミーの int型の仮引数を置くというルールになっています。

前置と後置とでは本体のコードの実装にも違いが生まれます。前置の場合、まずインクリメントやデクリメントを行ってからその結果を返す必要があります。後置の場合は、インクリメントやデクリメントをおこなう前の結果を残しておいて、インクリメントやデクリメントをしたあと、残しておいた結果を返す必要があります。そのため、前置の戻り値は参照型に、後置の戻り値は参照でない実体の型になります。

// 前置
C& operator++()
{
    // インクリメント
    return *this;
}

// 後置
C operator++(int)
{
    C tmp {*this};
    // インクリメント
    return tmp;
}

このような実装の違いが生まれることから、通常は前置のほうが効率的であることが期待できます

インクリメント演算子とデクリメント演算子のオーバーロードは、非静的なメンバ関数にする方法と、非メンバ関数にする方法の2種類があります。

非静的なメンバ関数による方法 🔗

非静的なメンバ関数にする方法では、前置の場合は仮引数のないメンバ関数を、後置の場合はダミーの int型引数をもつメンバ関数を定義します。オブジェクトx に対して演算子を適用すると、x.operator 演算子() のかたちで呼び出されます。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

    inline DataStore& operator++()   // 前置
    {
        ++m_value;
        return *this;
    }

    inline DataStore operator++(int) // 後置
    {
        DataStore tmp {*this};
        ++m_value;
        return tmp;
    }

    inline DataStore& operator--()   // 前置
    {
        --m_value;
        return *this;
    }

    inline DataStore operator--(int) // 後置
    {
        DataStore tmp {*this};
        --m_value;
        return tmp;
    }

private:
    int    m_value;
};

int main()
{
    DataStore ds1 {10};
    DataStore ds2 {0};

    ds2 = ++ds1;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";

    ds2 = ds1++;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";

    ds2 = --ds1;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";

    ds2 = ds1--;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";
}

実行結果:

11 11
12 11
11 11
10 11

非メンバ関数による方法 🔗

非メンバ関数にする方法では、前置の場合は対象のオブジェクトを仮引数にもつ関数を、後置の場合はさらにダミーの int型引数をもつ関数を定義します。オブジェクトx に対して演算子を適用すると、operator 演算子(x) のかたちで呼び出されます。

#include <iostream>

class DataStore {
    friend DataStore& operator++(DataStore& ds);
    friend DataStore operator++(DataStore& ds, int);
    friend DataStore& operator--(DataStore& ds);
    friend DataStore operator--(DataStore& ds, int);

public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

private:
    int    m_value;
};

DataStore& operator++(DataStore& ds)     // 前置
{
    ++ds.m_value;
    return ds;
}

DataStore operator++(DataStore& ds, int) // 後置
{
    DataStore tmp {ds};
    ++ds.m_value;
    return tmp;
}

DataStore& operator--(DataStore& ds)     // 前置
{
    --ds.m_value;
    return ds;
}

DataStore operator--(DataStore& ds, int) // 後置
{
    DataStore tmp {ds};
    --ds.m_value;
    return tmp;
}

int main()
{
    DataStore ds1 {10};
    DataStore ds2 {0};

    ds2 = ++ds1;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";

    ds2 = ds1++;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";

    ds2 = --ds1;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";

    ds2 = ds1--;
    std::cout << ds1.get_value() << " " << ds2.get_value() << "\n";
}

実行結果:

11 11
12 11
11 11
10 11

二項演算子 🔗

続いて、以下の二項演算子です。

+   -   *   /   %
^   &   |   <<  >>
+=  -=  *=  /=  %=  ^=  &=  |=  >>= <<=
==  !=  <   >   <=  >=
&&  ||
,

ここに含まれていない = については代入演算子のルールに従います。

+-*& には単項演算子のものも存在しますが、そららは単項演算子のルールに従います。

+-*/% は四則演算の意味を持った演算子なので、基本的にはそれらの意味を逸脱しないようにオーバーロードします。ただし、+ を文字列の連結に用いるような、よく知られている使い方は許されると考えていいでしょう。

^&|<<>> はビット演算をおこなう演算子です。こちらも基本的には意味を変えないほうがいいですが、std::cout << "Hello" のように、シフト演算子を「処理の方向」を表すものと見立てて使う例もあります。

+=-=*=/=%=^=&=|=>>=<<= は複合代入演算子です。イコールを外した部分の演算子と合わせてオーバーロードして、効果が食い違わないようにするべきです(たとえば、+= でなら連結できるのに + だと連結できないようなことを避ける)。int などの標準的な型なら許されている a += (b += c); のような式を可能にするために、戻り値を参照型にします。

==!=<><=>= は等価演算子📘と関係演算子📘です。結果は真か偽になるはずなので、bool型の戻り値を返すように実装します。また、あとで解説するように、互いを呼び出すかたちで実装することが推奨されます。

&&|| は論理AND と論理OR の演算子です。演算子関数を呼び出す動作になってしまうことにより、実引数を作るために右オペランドも必ず評価されることになり、これらの演算子が本来もつ、短絡評価(「要素を探索する」のページを参照)の機能が働かなくなります5。また、左オペランドと右オペランドの評価順にも保証がなくなります。こうした混乱を避けるために、原則として &&|| はオーバーロードしないことを勧めます6

, はコンマ演算子です(「if文と条件演算子」のページを参照)。この演算子もオーバーロードによって演算子関数を呼び出す動作になると、左オペランドと右オペランドのどちらが先に評価されるか保証がなくなるため、左オペランドの評価結果が捨てられ、右オペランドの評価結果が全体の評価結果になるルールに保証が持てなくなります。混乱を割けるために、原則として , はオーバーロードしないことを勧めます。6


二項演算子のオーバーロードには、非静的なメンバ関数にする方法と、非メンバ関数にする方法の2種類があります。

非静的なメンバ関数による方法 🔗

非静的なメンバ関数にする方法では、右オペランドに対応する仮引数をもったメンバ関数を定義します。オブジェクトx に対して演算子@を x @ y のように適用すると、x.operator 演算子(y) のかたちで呼び出されます。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

    DataStore operator+(int v) const;
    DataStore operator-(int v) const;

    DataStore& operator+=(const DataStore& rhs);
    DataStore& operator-=(const DataStore& rhs);

private:
    int    m_value;
};

DataStore DataStore::operator+(int v) const
{
    DataStore tmp {*this};
    tmp.m_value += v;
    return tmp;
}

DataStore DataStore::operator-(int v) const
{
    DataStore tmp {*this};
    tmp.m_value -= v;
    return tmp;
}

DataStore& DataStore::operator+=(const DataStore& rhs)
{
    this->m_value += rhs.m_value;
    return *this;
}

DataStore& DataStore::operator-=(const DataStore& rhs)
{
    this->m_value -= rhs.m_value;
    return *this;
}

int main()
{
    DataStore ds1 {100};
    DataStore ds2 {200};

    std::cout << (ds1 + 10).get_value() << "\n"; 
    std::cout << (ds2 - 10).get_value() << "\n"; 

    ds1 += ds2;
    std::cout << (ds1).get_value() << "\n";

    ds1 -= ds2;
    std::cout << (ds1).get_value() << "\n";
}

実行結果:

110
190
300
100

この方法では、左オペランドのメンバ関数として呼び出されることになるため、たとえば 10 + ds1 のように、右オペランド側を DataStore型にしようとするとコンパイルエラーになります。しかし、加算を意味するのであれば通常、ds1 + 1010 + ds1 は同等の結果になるべきでしょう。そうであれば、非メンバ関数による方法で実装しなければなりません。

非メンバ関数による方法 🔗

非メンバ関数にする方法では、2つのオペランドに対応した仮引数をもった関数を定義します。演算子@ を x @ y のように適用すると、operator 演算子(x, y) のかたちで呼び出されます。

この方法を使えば、y @ x のようにオペランドを入れ替えたかたちで使用される場合にも対応できます。次のサンプルプログラムでは、DataStore + intint + DataStore をどちらも受け付けられるようにしています。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

private:
    int    m_value;
};

DataStore operator+(const DataStore& ds, int v)
{
    DataStore tmp {ds.get_value() + v};
    return tmp;
}

DataStore operator+(int v, const DataStore& ds)
{
    DataStore tmp {ds.get_value() + v};
    return tmp;
}

int main()
{
    DataStore ds {100};

    std::cout << (ds + 10).get_value() << "\n"; 
    std::cout << (10 + ds).get_value() << "\n"; 
}

実行結果:

110
110

非メンバ関数として実装しようとすると、クラス型の非公開なメンバにアクセスできなくて困ることがあります。そのようなときには、下手に public なメンバ関数を作って穴を開けるのではなく、演算子関数をフレンド関数として指定します(「関数テンプレート」のページを参照)。

比較の実装 🔗

比較の意味合いで、等価演算子や関係演算子のオーバーロードをするときには、等価演算子2つか、等価演算子と関係演算子あわせて6つをまとめてオーバーロードしなければ、使用者が混乱する恐れがあります。素直にすべてを定義すると面倒で間違いやすく、データメンバが増えたときの保守性の面でも問題があります。

素直に6個の演算子関数を定義するとすれば、次のようになるでしょう。

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

    inline bool operator==(const DataStore& other) const
    {
        return m_value == other.m_value;
    }
    inline bool operator!=(const DataStore& other) const
    {
        return m_value != other.m_value;
    }
    inline bool operator<(const DataStore& other) const
    {
        return m_value < other.m_value;
    }
    inline bool operator>(const DataStore& other) const
    {
        return m_value > other.m_value;
    }
    inline bool operator<=(const DataStore& other) const
    {
        return m_value <= other.m_value;
    }
    inline bool operator>=(const DataStore& other) const
    {
        return m_value >= other.m_value;
    }

private:
    int    m_value;
};

これでも正常に動作しますが、あとからデータメンバが追加されるなどの変更があると、書き換える箇所が多くなりますし、どこか1か所だけミスをするというような分かりづらいバグを入れ込む恐れもあります。

そこで、互いの実装を利用することで、データメンバを直接比較しているコードをできるだけ減らすようにします。

まず、!= の結果は == の結果の真偽を反対にしたものですから、operator== を呼び出して結果を反転して返すように実装すればいいことが分かります。

    // != は == の反対
    inline bool operator!=(const DataStore& other) const
    {
        return !(*this == other);
    }

<= はどうでしょう。<= の反対は > だという考え方で、さきほどと同じように > の結果を反転させる方法で実装することもできますが、ここでは別の見方をしたほうがより良くなります。<= は、<== のいずれかの結果が true になるときに true になるものと考えて、次のように実装します。

    // <= は < と == を合わせたもの
    inline bool operator<=(const DataStore& other) const
    {
        return (*this < other) || (*this == other);
    }

残りの >>= はそれぞれ、<=< の反対として実装しましょう。

    // > は <= の反対
    inline bool operator>(const DataStore& other) const
    {
        return !(*this <= other);
    }
    // >= は < の反対
    inline bool operator>=(const DataStore& other) const
    {
        return !(*this < other);
    }

このように実装すると、実際にデータメンバを比較しているコードは operator==operator< の2箇所に集約され、ほかの4つの演算子関数のコードは、データメンバが変更されたとしても書きなおす必要がなくなります。

コード全体は次のようになります。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

    inline bool operator==(const DataStore& other) const
    {
        return m_value == other.m_value;
    }
    inline bool operator!=(const DataStore& other) const
    {
        return !(*this == other);
    }
    inline bool operator<(const DataStore& other) const
    {
        return m_value < other.m_value;
    }
    inline bool operator>(const DataStore& other) const
    {
        return !(*this <= other);
    }
    inline bool operator<=(const DataStore& other) const
    {
        return (*this < other) || (*this == other);
    }
    inline bool operator>=(const DataStore& other) const
    {
        return !(*this < other);
    }

private:
    int    m_value;
};

int main()
{
    DataStore ds1 {10};
    DataStore ds2 {0};

    std::cout << std::boolalpha
              << (ds1 == ds2) << "\n"
              << (ds1 != ds2) << "\n"
              << (ds1 < ds2) << "\n"
              << (ds1 > ds2) << "\n"
              << (ds1 <= ds2) << "\n"
              << (ds1 >= ds2) << "\n";
}

実行結果:

false
true
false
true
false
true

【C++98/03 経験者】これと同じことを、std::rel_ops の助けを借りておこなうことがありましたが、下のコラムにあるように、C++20 で追加された機能によって完全に不要なものとなったため、非推奨になっています7

【C++20】三方比較演算子📘 (<=>) が追加され、たった1つの演算子関数(operator <=>)を定義するだけで、6つの演算子がまとめてオーバーロードできるようになりました8

代入演算子 🔗

続いて、代入演算子です。

=

複合代入演算子は、二項演算子にあります。

代入演算子のオーバーロードは、非静的なメンバ関数として実装しなければならず、非メンバ関数にはできません。

仮引数は1つです。戻り値の型は、a = b = c; のような式を可能にするため、通常は参照型にして *this を返すように実装します。

x = y のように適用すると、x.operator=(y) のかたちで呼び出されます。

#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;
    }

    inline DataStore& operator=(int v)
    {
        m_value = v;
        return *this;
    }

private:
    int    m_value;
};

int main()
{
    DataStore ds1 {100};
    DataStore ds2 {200};
    DataStore ds3 {300};

    ds2 = ds1;
    std::cout << (ds1).get_value() << " " << (ds2).get_value() << "\n";

    ds1 = ds2 = ds3;
    std::cout << (ds1).get_value() << " " << (ds2).get_value() << " " << (ds3).get_value() << "\n";

    ds3 = 500;
    std::cout << (ds3).get_value() << "\n";
}

実行結果:

100 100
300 300 300
500

仮引数を自身のクラス型の const参照にする実装では、同じクラス型のオブジェクトをコピーする代入動作を実現したものにするべきです。つまり、*this のデータメンバが、渡されてきたオブジェクトと同じになるように実装します。このような代入演算子を特別に、コピー代入演算子 (copy assignment operator) と呼びます。

【上級】仮引数を自身のクラスの右辺値参照にする実装では、ムーブ代入をおこなうように実装します。これはムーブ代入演算子と呼ばれます。

コピー代入演算子は、コンストラクタとよく似た生成のルールを持っており(「コンストラクタ」のページを参照)、プログラマーが明示的に定義すればそれを使い、明示的に定義しなければコンパイラが自動的に生成します。

【上級】自動生成のルールは少々複雑で、ムーブ代入演算子やムーブコンストラクタを明示的に定義すると、コピー代入演算子はデフォルトでは =delete によって削除されるなど、ほかの特殊なメンバ関数の定義からの影響を受けます。また、コピーコンストラクタやデストラクタを明示的に定義したのに、コピー代入演算子を定義しないことは推奨されません。9

自動生成されたコピー代入演算子は、すべての(非静的な)データメンバをコピー代入するように実装されます。

=default(「コンストラクタ」のページを参照)を使えば、コンパイラが自動生成するコピー代入演算子をあえて明示的に記述できます。

class DataStore {
public:
    DataStore& operator=(const DataStore& src) = default;
};

また、=delete(「静的メンバ」のページを参照)を使ってコンパイラの自動生成したコピー代入演算子を削除し、コピー代入を不可能にできます。

class DataStore {
public:
    DataStore& operator=(const DataStore& src) = delete;
};

【C++98/03 経験者】コピーを禁止するために、コピー代入演算子(とコピーコンストラクタ)を private にする方法を取ることがありましたが、C++11 以降であれば =delete で削除するほうが明確です。

関数呼び出し演算子 🔗

続いて、関数呼び出し演算子です。

()

関数呼び出し演算子のオーバーロードは、非静的なメンバ関数として実装しなければならず、非メンバ関数にはできません。

【C++23】非静的でなければならない条件が削除されました。operator() の実装がメンバへのアクセスを必要としていないのなら、静的メンバ関数とすることで thisポインタを渡す分のコストを避けられます。 11

関数呼び出し演算子の演算子関数は operator() という名前ですが、これは関数オブジェクトのところで登場しています(「シャッフルと乱数」のページを参照)。つまり、operator() を定義するということは、関数オブジェクトとして機能するクラスを定義するということです。

仮引数や戻り値の制限は特になく、都合が良いように好きに実装が行えます。

x(args) のように適用すると、x.operator()(args) のかたちで呼び出されます。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

private:
    int    m_value;
};

struct DataStorePrinter {
    inline void operator()(const DataStore& ds) const
    {
        std::cout << ds.get_value() << "\n";
    }
};

int main()
{
    DataStore ds1 {100};
    DataStore ds2 {200};
    DataStorePrinter dsprint {};

    dsprint(ds1);
    dsprint(ds2);
}

実行結果:

100
200

クラスメンバアクセス演算子 🔗

続いて、クラスメンバアクセス演算子です。文字通り、クラスのメンバにアクセスするための演算子で、..* もありますが、オーバーロードできるのは以下のものだけです。

->  ->*

クラスメンバアクセス演算子のオーバーロードは、非静的なメンバ関数として実装しなければならず、非メンバ関数にはできません。仮引数はありません。

-> はポインタを経由してメンバにアクセスするものなので、ポインタの動作を真似ようとしてオーバーロードするのなら、間接参照の * を併せてオーバーロードすることを検討しましょう。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}

    inline int get_value() const
    {
        return m_value;
    }

private:
    int    m_value;
};

class DataStorePtr {
public:
    explicit DataStorePtr(DataStore* pDs) : mpDataStore {pDs}
    {}

    inline DataStore* operator->()
    {
        return mpDataStore;
    }
    inline const DataStore* operator->() const
    {
        return mpDataStore;
    }

    inline DataStore& operator*()
    {
        return *mpDataStore;
    }
    inline const DataStore& operator*() const
    {
        return *mpDataStore;
    }

private:
    DataStore*  mpDataStore;
};


int main()
{
    DataStore ds {100};

    const DataStorePtr dscp {&ds};
    std::cout << dscp->get_value() << "\n";
    std::cout << (*dscp).get_value() << "\n";

    DataStorePtr dsp {&ds};
    std::cout << dsp->get_value() << "\n";
    std::cout << (*dsp).get_value() << "\n";
}

実行結果:

100
100
100
100

->* はメンバポインタを使ってメンバにアクセスする演算子です(「構造体とポインタ」のページを参照)。->* をオーバーロードする機会はあまりないですが、たとえば次のようになります。

#include <iostream>

struct DataStore {
    int    m_value {};
};

class DataStorePtr {
public:
    explicit DataStorePtr(DataStore* pDs) : mpDataStore {pDs}
    {}

    inline int operator->*(int DataStore::*member)
    {
        return mpDataStore->*member;
    }
    inline int operator->*(int DataStore::*member) const
    {
        return mpDataStore->*member;
    }

private:
    DataStore*  mpDataStore;
};


int main()
{
    DataStore ds {100};
    int DataStore::*pDsValue {&DataStore::m_value};

    const DataStorePtr dscp {&ds};
    std::cout << dscp->*pDsValue << "\n";

    DataStorePtr dsp {&ds};
    std::cout << dsp->*pDsValue << "\n";
}

実行結果:

100
100

new演算子、delete演算子 🔗

new演算子、delete演算子は以下の4種類です。

new
delete
new[]
delete[]

これらの演算子はここまでのページに登場したことがなく、話題としても比較的大きなものになるので、ここでは詳細は取り上げないことにします。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

std::string のオブジェクトs1、s2 があるとき、s1 + s2s1 + "xyz""abc" + s2 が可能であるのはどのような仕組みによるものと考えられますか? また、"abc" + "xyz" が行えないのはなぜですか?

解答・解説

問題2 (基本★★)

&&||, をオーバーロードすると、オペランドが評価されるルールに変化が起こることについて説明してください。

解答・解説

問題3 (基本★★)

RingBufferクラステンプレートに以下の演算子をオーバーロードしてください。

現状の RingBufferクラステンプレートの実装は、「オーバーロード」のページにあります。

解答・解説

問題4 (応用★★★)

RingBufferクラステンプレートに、std::vector などのコンテナにあるようなイテレータの機能を実装してください。要件は以下のとおりです。

解答・解説

問題5 (応用★★★)

RingBufferクラステンプレートに、constイテレータを実装してください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る