はじめに
この記事ではC++の演算子のオーバーロードを使い慣れていない人の為のまとめとして作成しました。
C++の演算子のオーバーロード、引数や戻り値に自由が利きすぎてどうするのがセオリーなのか分からないぞ、というようなときに見て頂けたらと思います。
主に引数や戻り値を中心に据えますが、細かい機能も網羅していきたいと考えています。
私は二項演算子はグローバル関数教の信徒であり、const教の敬虔な信徒です。
優先順位の順に、演算子別に解説します。
なお、this
の型を T* const
型とし、T
とは別の任意の型をU
としています。
では、早速解説していきたいと思います。
Member Selection
U* operator->() const noexcept ;
メンバ選択演算子。ドット演算子はオーバーロードできないので、載せるのはアロー演算子だけになります。この演算子はメンバ関数でなければなりません。また、演算子の文脈で使うにはアロー演算子を持った任意の型を返す必要があります。多くの場合は生ポインタを使うことになると思います。注意すべきは、基本的に戻り値はconstにしてはならないこと、メンバ関数はconst指定しなければならないことです。
以下のように、constとそうでないバージョンを用意するのは多くの場合誤りです。
const U* operator->() const noexcept;
U* operator->() noexcept;
アロー演算子について、2016/12/24 追記:
これはアロー演算子を持つスマートポインタやイテレータと、それが示す要素との関係を考えるとよく理解できます。
template< class T >
class pointer
{
T* ptr;
public:
T* // 戻り値のconstはこのポインタの要素の変更の可否を示す
operator->() const noexcept // ここのconstはこのポインタの変更の可否を示す
{ return ptr; }
};
...
pointer<int> p = &value;
*p = 12; // p の中身は変わるけどp は変わらない; アロー演算子で言う戻り値のconstに関わる
p = &another; // p の中身は変わらないけどp は変わる; アロー演算子で言うconst修飾に関わる
p = nullptr; // p の中身は変わらないけどp は変わる
上の例では、アロー演算子の示す要素とオブジェクトの状態の間には関係がありません。
ただし、optional
に代表されるような特殊な使われ方をするときは例外です。アロー演算子は何を示すのか、オブジェクトとアロー演算子の示すものの関係はどうであるか、しっかり考えなければなりませんね。以下の例では、アロー演算子の示す要素を変更するとはすなわちその要素を包むオブジェクトを変更することだと言えます。
template< class T >
class optional
{
T value;
public:
const T* operator->() const noexcept { return &value; }
T* operator->() noexcept { return &value; }
};
...
optional<int> n = 1;
*n = 123; // n の中身が変わることはすなわちn が変わることを意味する
n = 456; // この文脈では *n = 456 に等価な操作
n = nullopt; // n を変える処理。*n = nullopt とは書き換えられない。
使用例。
std::unique_ptr<std::string> ptr(new string());
ptr->push_back('a'); // ptr.operator->()->push_back('a'); と全く等価。
Array Subscript
const U& operator[](std::size_t) const ;
U& operator[](std::size_t) ;
// 安全性を高めたければ、以下のように3つのバージョンを用意してもよい。
const U& operator[](std::size_t) const& ;
U& operator[](std::size_t) &;
U operator[](std::size_t) const&& ;
添字演算子。この演算子はメンバ関数でなくてはなりません。多くの場合constバージョンと非constバージョンの両方を用意しなくてはなりまけん。引数は例としてstd::size_t
を入れていますが、コンテナの性質によって様々になります。戻り値はプリミティブの参照型でなくても大丈夫ですが、非constバージョンではコンテナに値を設定できるような仕組みがなくてはなりません。
std::bitset<8> bit {};
// bool& elem = bit[0]; // std::vectorなどでは偶然使えるが、これは実はcontainerコンセプトには含まれない。
auto&& elem = bit[0]; // 参照型に束縛したければ、universal reference を使うのが確実。
bit[0] = U(); // 非constのoperator[]はこれを保証しなくてはならない。
下の3つのバージョンというのは、上2つが一時オブジェクトが内部に保持するオブジェクトへの参照を返しうるのに対し、それを防ぐように改めたバージョンと言えます。上2つでは以下のような問題が発生しえます。
int& n = std::vector<int>{1, 2, 3}[0]; // oops, n is a dangling reference.
auto&& m = std::vector<int>{1, 2, 3}[1]; // the same problem occurs.
これを防ぐため、右辺値の場合に限って参照ではなく実体を返すようにします。
template<class T, class Alloc = std::allocator<T>>
class my_vector
{
std::vector<T, Alloc> vec;
public:
my_vector(std::initializer_list<T> init) : vec{init} {}
T& operator[](std::size_t n) & { return vec[n]; }
const T& operator[](std::size_t n) const& { return vec[n]; }
T operator[](std::size_t n) const&& { return std::move(vec[n]); } // 手動ムーブ。noexceptがあってもよい。
};
自作コンテナを作るならこれくらい気の利いたものを作りたいですね。
int& n = my_vector<int>{1, 2, 3}[0]; // コンパイルエラー
auto&& m = my_vector<int>{1, 2, 3}[1]; // OK. mはint&&と推論され、参照先の一時オブジェクトはmと同じだけ延命される。
Function Call
U operator()(Args...) ;
関数呼び出し演算子。この演算子はメンバ関数でなければなりません。戻り値、引数、const指定など全くの自由です。最もよく使うのは述語としてアルゴリズム関数に突っ込んだりですかね。ラムダ式にお株を奪われてしまいましたけど。
後は状態を持った関数をstatic変数を使わずに実現したいときなどに使います。そのようなオブジェクトをC++では特に関数オブジェクトと呼びます。
以下にC++14時点でのstd::plus
の実装例を示します。
namespace std
{
template<class T = void>
class plus
{
public:
constexpr T operator()(const T& t1, const T& t2) const { return t1 + t2; }
};
template<>
class plus<void>
{
public:
template<class T, class U>
constexpr auto operator()(const T& t, const U& u) const { return t + u; }
};
}
実際に使うとこんな感じ。テンプレートパラメータにvoidを指定すると引数と戻り値を推論してくれる。ほのかなジェネリックの香り。
std::plus<> plus;
int n = plus(1, 2); // n == 3
Postfix Increment/Decrement
T operator++(int) ;
T operator--(int) ;
後置インクリメント/デクリメント。この演算子はメンバ関数であるべきです。const指定はなしで、予め保存しておいた変更前のthisを返却すべきです。引数になにが渡されるかは謎です。
int n = 1;
int m = n++; // mには1が入る。
Prefix Increment/Decremrnt
T& operator++() ;
T& operator--() ;
前置インクリメント/デクリメント。この演算子はメンバ関数であるべきです。const指定はなしで、this参照を返却すべきです。
int n = 1;
int m = ++n; // mには2が入る。
One's Complement
T operator~() const ;
ビット否定演算子。1の補数を返します。この演算子はメンバ関数であるべきです。たまに勘違いされますが、この演算子は呼び出し元には変更を加えるべきではありません。
int n = 0 ^ 0; // 全てのビットを伏せた状態で初期化。
n = ~n; // 全てのビットを立てる。
Logical Not
bool operator!() const noexcept {
return !static_cast<bool>(*this);
}
論理否定演算子。この演算子はメンバ関数であるべきです。また戻り値がboolに暗黙変換可能な型であり、const指定がされていて、例外を送出しない関数とするべきです。
多くの場合、この演算子を実装する型Tはbool型へ変換可能な型であり、ここでは最も典型的な実装を例として定義してみました。
std::unique_ptr<int> ptr(new int());
if (ptr) { /* なんか */ }
if (!ptr) { /* なんか */ }
Unary Negation/Plus
T operator-() const ;
T operator+() const ;
単項マイナス演算子と単項プラス演算子。この演算子はメンバ関数であるべきです。また、const指定されてなくてはなりません。
int n = 100;
int m = +n;
int l = -m;
Address-of
T* operator&() const noexcept ;
アドレス取得演算子。この演算子は基本的にオーバーロードすべきではありませんが、c++98などでnullptrを自前で実装するときなどには使うかもしれません。
class nullptr_t
{
/*ごにょごにょ*/
private:
nullptr_t* operator&() const noexcept ; // アドレス取得を禁止
};
static constexpr nullptr_t nullptr; // このオブジェクトのアドレスは取得できない。
まあ、std::addressof
関数を使えばどうってことないのですが、こんな関数がある理由になる程度にはアドレス取得演算子をオーバーロードすべきではないということですね。
Indirection
T& operator*() const noexcept ;
関節参照演算子。この演算子はメンバ関数でなければなりません。アロー演算子同様、この演算子は常にconst指定をして例外を送出しないようにするべきです。この演算子ではconst指定と戻り値のconstは全く関係ないことに気をつけてください。
std::unique_ptr<int> ptr(new int());
int& elem = *ptr;
この関数でも添字演算子と同じ問題が発生し得るのですが、この演算子はその目的上右辺値参照のオーバーロードはしない方がよいと思われます。以下のようなケースが大いにあり得るからです。
something s;
something& rs = *my_pointer_wrapper<something>(&s);
Create/Destroy Object
new/delete演算子は非常に深い話題であるのでこの記事に含めることはしないことにしました。
2016/10/01 追記: 別に記事をご用意いたしました。よろしければこちらもご覧ください。
new/delete 演算子のオーバーロード
Cast
explicit(optional) operator U() const noexcept ;
キャスト演算子。この演算子はメンバ関数でなければなりません。また、この演算子には戻り値は必要ありません。C++11からこの演算子にはexplicitを付けることができ、これを指定した場合暗黙の変換を防いでくれます。
混乱される方が多いですが、他の型からこの型に変換するにはコンストラクタ、この型から他の型に変換するにはキャスト演算子を使います。両者は依存関係をよく考えて使うようにしてください。
class something
{
public:
// ...
// 最もよく使うbool型への変換
// 基本的にはexplicitがあった方がよい。
// ないと、boolに暗黙変換されてintに型昇格されて…という具合に意味分からない型変換に振り回される。
explicit operator bool() const noexcept { return is_empty(); }
bool operator!() const noexcept { return !static_cast<bool>(*this); } // 大抵ワンセット
};
このsomething型は、以下のように利用できる。
something s;
if (s) { /*do something*/ }
if (!s) { /* do something */ }
// int n = s + 1; // oops! boolへの暗黙変換は禁じられている。explicitを付けなければこの操作は可能になる。
Pointer-To-Member
V operator->*(V(U::*)(Args)) const noexcept ;
V operator->*(V U::*) const noexcept ;
メンバへのポインタ演算子。この演算子はメンバ関数でなければなりません。アロー演算子とほぼ同じものですが、こちらは引数に任意のメンバ関数/オブジェクトへのポインタを取ります。利用する機会も少ないので、ポインタのラッパを作りたい時に、更にこだわってみたい時にでもという感じです。
Arithmetic
T operator*(const T& t1, const T& t2) { return T(t1) *= t2; }
T operator/(const T& t1, const T& t2) { return T(t1) /= t2; }
T operator%(const T& t1, const T& t2) { return T(t1) %= t2; }
T operator+(const T& t1, const T& t2) { return T(t1) += t2; }
T operator-(const T& t1, const T& t2) { return T(t1) -= t2; }
Diff operator-(const T&, const T& t2) ; // ポインタやイテレータの場合 ...(*1)
T operator-(const T& t, Diff diff) { return t -= diff; } // ポインタやイテレータの場合 ...(*2)
算術演算子。これら演算子はグローバル関数であるべきです。これらの算術演算子は通常複合代入演算子と同時にオーバーロードされるので、機械的に書くことができます。(*1を除く)
減算演算子だけは色々と都合があり、数学的な型に実装された場合とポインタやイテレータに実装された場合とで大きく性質が変わります。特に、Diff operator-(const T&, const T&) ;
というパターンの減算はフレンド指定されることが多いです。私は二項演算子グローバル関数教徒なので、フレンドをハラームに指定する宗教の人はここではどうかご容赦ください。
int n = 123, m = 456;
int l = n - m;
int array[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
int* pbegin = array;
int* pend = array + 10;
std::ptrdiff_t diff = pend - pbegin; // ポインタとポインタの差はstd::ptrdiff_t型で定義される。(*1)
int i = *(pend - 5); // ポインタからstd::ptrdiff_t型を引くとポインタ型が返る。(*2)
Shift
T operator<<(const T& t, std::size_t n) { return T(t) <<= n; }
T operator>>(const T& t, std::size_t n) { return T(t) >>= n; }
template<class Char, class Traits>
std::basic_ostream<Char, Traits>& operator<<(
std::basic_ostream<Char, Traits>& os, const T&) ;
template<class Char, class Traits>
std::basic_istream<Char, Traits>& operator>>(
std::basic_istream<Char, Traits>& is, const T&) ;
シフト演算子。これらの演算子はグローバル関数であるべきです。算術演算子同様、通常複合代入演算子と同時にオーバーロードされるので機械的に書くことができます。
C++では標準ライブラリが禁忌とされる演算子のオーバーロードの悪用をしていますので、ここでストリーム演算子についても解説します。下2つの宣言は出入力演算子やストリーム演算子などと呼ばれるもので、必ずグローバル関数でなければなりません。これを定義することで、std::cout
などに自作の型を流せるようになります。
Compare
bool operator<(const T&, const T&) ;
bool operator>(const T& t1, const T& t2) { return t2 < t1; }
bool operator<=(const T& t1, const T& t2) { return !(t1 > t2); }
bool operator>=(const T& t1, const T& t2) { return !(t1 < t2); }
比較演算子。これらの演算子はグローバル関数であるべきです。実は小なりさえ定義してしまえば後は機械的に書くことができます。というか、そのような実装になっていなければなりません。
この演算子はそんなに書くこともないです。
Equality Compare
bool operator==(const T&, const T&) ;
bool operator==(const T& t1, const T& t2) { return !(t1 < t2) && !(t1 > t2); } // ...(*1)
bool operator!=(const T& t1, const T& t2) { return !(t1 == t2); }
等価比較演算子。これらの関数はグローバル関数であるべきです。比較演算子を定義しているクラスでは、その意味論によってはこれらの関数は全く機械的に書くことができます(*1)。
Bitwise
T operator&(const T& t1, const T& t2) { return ~(~t1 | ~t2); }
T operator^(const T& t1, const T& t2) { return (t1 & ~t2) | (~t1) & t2; }
T operator|(const T&, const T&) ;
ビット演算子。これらの演算子はグローバル関数であるべきです。論理和とビット否定があれば、論理積と排他的論理和は機械的に書くことができます。
Logical And/Or
bool operator&&(const T& t1, const T& t2) { return !(!t1 || !t2); }
bool operator||(const T&, const T&) ;
論理演算子。論理演算子って言い方はあまりしませんね。これらの演算子はグローバル関数であるべきです。論理和と論理否定があれば論理積は機械的に書くことができます。
Assignment
T& operator=(const U&) ;
T& operator=(const T&) ;
T& operator=(T&&) noexcept ;
// こっちの方が安全? 自然?
T& operator=(const U&) &;
T& operator=(const T&) &;
T& operator=(T&&) & noexcept;
代入演算子。この演算子はメンバ関数でなければなりません。引数には任意の型U
を取りますが、典型的には自身と同じ型への参照を取るコピー代入演算子とムーブ代入演算子が実装されます。ムーブ代入演算子は例外を送出するべきではありません。戻り値にthis参照を返します。
お尻にアンパサンドがついているバージョンは、右辺値に代入が行われることを禁じています。間違ってもこれを防ぐために戻り値の型にconstを付けたりしないでください。
// プリミティブ型ではこれは許されない。
1 = 2;
// 通常これは許されているが、アンパサンド付きのバージョンだとコンパイルエラーになる。
// 不自然に見えるが危険は少ない。
something() = something();
something s1, s2;
s1 = s2 = something(); // これができるのはthis参照を返すおかげ。
Compound Assignment
T& operator*=(const T&) ;
T& operator/=(const T&) ;
T& operator%=(const T&) ;
T& operator+=(const T&) ;
T& operator-=(const T&) ;
T& operator<<=(const T&) ;
T& operator>>=(const T&) ;
T& operator&=(const T&) ;
T& operator|=(const T&) ;
T& operator^=(const T&) ;
複合代入演算子。これらの演算子はメンバ関数でなければなりません。引数には任意の型U
を取りますが、const T&
が使われることが多い印象です。注意点は全て代入演算子と同じです。ここであの二項演算子群の中核を担うことになりますね。
Comma
// メンバ関数バージョン
U& operator,(U&) const noexcept ;
// グローバル関数バージョン
U& operator,(const T&, U&) noexcept ;
コンマ演算子。この演算子はオーバーロードするべきではありません。でも一応するなら、二項演算子ではありますがメンバ関数の方が良い気がします。
比較的有名なライブラリでオーバーロードされてたりされてなかったり。
どこかで見た有用な使い方は、Mutexのロック機構に使うとかいうものでした。
// scoped_guardのコンストラクタでロックされ、func()が実行され、scoped_guardのデストラクタでロックが解除される。
// mutexなんかでは使いやすいかも?
scoped_guard(), func();
{ // コンマハックを使わなければこうなる。
scoped_guard guard;
func();
}
下の方が意図が明確でいいような気もしなくもない。
おまけ
std::rel_ops
名前空間とBoost.Operatorsに軽く触れようと思います。
ここまでで、多くの演算子について機械的に書くことができると言いましたが、機械的に書くことができる、というのは決していいところではありません。最も私たちの忌諱すべきところともいえると思います。そういう部分を自動的に定義してくれるものが標準ライブラリと、準標準ライブラリにもあるので紹介したいと思います。
std::rel_ops
<utility>
ヘッダに入っているstd::rel_ops
名前空間には、!=,>,<=,>= の4つの演算子が定義されており、それぞれ以下のように宣言されています。
namespace std {
namespace rel_ops
{ // namespace std::rel_ops
template< class T >
bool operator!=( const T& lhs, const T& rhs );
template< class T >
bool operator>( const T& lhs, const T& rhs );
template< class T >
bool operator<=( const T& lhs, const T& rhs );
template< class T >
bool operator>=( const T& lhs, const T& rhs );
} // namespace rel_ops
} // namespace std
この名前空間をusingすれば、自作クラスの比較演算子を定義する手間が省けるという寸法です。
なんか標準ライブラリにしてはあまりに適当というか雑というか、そんな気がしますけれど。
そんな気がしたなら、次に行きましょう。boost様がお待ちです。
Boost.Operators
Boostは標準ライブラリではありませんが、それを補って余りあるパワーと、安全性と、移植性があります。コンパイル時間もブーストしてくれます。Boost.Operatorsはstd::rel_ops
とは比較にならないほど演算子の自動定義をサポートしてくれます。具体的には、欲しい演算子を持つクラスをprivate継承することでmix-inという形で演算子を個別に持ってくることができます。
例として、オリジナルの整数型を作るとします。そうなるとものすごい量の演算子をオーバーロードをしなくてはなりませんね。
class my_integral
: boost::operators<my_integral>
{
friend bool operator==(const my_integral&, const my_integral&) ;
friend bool operator<(const my_integral&, const my_integral&) ;
public:
my_integral& operator++() ;
my_integral& operator--() ;
my_integral& operator*=(const my_integral& n) ;
my_integral& operator/=(const my_integral& n) ;
my_integral& operator%=(const my_integral& n) ;
my_integral& operator+=(const my_integral& n) ;
my_integral& operator-=(const my_integral& n) ;
my_integral& operator<<=(const my_integral&) ;
my_integral& operator>>=(const my_integral&) ;
my_integral& operator&=(const my_integral&) ;
my_integral& operator|=(const my_integral&) ;
my_integral& operator^=(const my_integral&) ;
};
bool operator==(const my_integral&, const my_integral&) ;
bool operator<(const my_integral&, const my_integral&) ;
自動で定義できる二項演算子などは全てboost::operators<my_integral>
の部分でmix-inされています。よって、自前で定義するのは以下だけでよくなります。
- 前置インクリメント、デクリメント
- 複合代入演算子
- 等価比較演算子、小なり演算子
他にも、std::rel_ops
ではサポートされない、右辺に任意の型U
を取る機能や、イテレータの作成に必要な演算子の自動定義など、多くの機能が利用できます。この機能はヘッダオンリーで使用できますので、ぜひ覗いてみてくださいね。
おまけ2
自作のポインタ型のラッパ型を作ってみましょう。そもそもプリミティブ型って、ポインタも含めて、オブジェクトじゃないんですよね。継承できないし。オブジェクト指向的には中途半端なんです。みたいな不満を抱いたとして、ポインタ型を作ってみましょう。
二項演算子たちは本当にただ面倒くさいだけなので書きません。Boost.Operators使うか、自分で似たの作りましょう。
template<class T>
class pointer
{
using this_type = pointer<T>;
T* _ptr;
public:
using value_type = T;
using reference = value_type&;
using const_reference = const value_type&;
using difference_type = std::ptrdiff_t;
pointer() noexcept : _ptr() {}
pointer(T* p) noexcept : _ptr(p) {}
T* get() const noexcept { return _ptr; }
T* operator->() const noexcept { return _ptr; }
reference operator*() const noexcept { return (*_ptr); }
// この場合の添え字演算子は関節参照演算子のシンタックスシュガー
reference operator[](std::ptrdiff_t diff) { return *(_ptr + diff); }
const_reference operator[](std::ptrdiff_t diff) const { return *(_ptr + diff); }
reference operator++() { ++_ptr; return (*this); }
reference operator--() { --_ptr; return (*this); }
value_type operator++(int) { this_type temp(*this); ++*this; return temp; }
value_type operator--(int) { this_type temp(*this); --*this; return temp; }
explicit operator bool() const noexcept { return _ptr; }
bool operator!() const noexcept { return !static_cast<bool>(*this); }
template<class U,
std::enable_if_t<std::is_member_function_pointer<U>::value, std::nullptr_t> = nullptr>
auto operator->*(U u) const noexcept
{
return [u, this](auto... args)
{ // lambda
return (_ptr->*u)(std::forward<decltype(args)>(args)...);
};
}
template<class U,
std::enable_if_t<std::is_member_object_pointer<U>::value, std::nullptr_t> = nullptr>
auto& operator->*(U u) const noexcept
{
return _ptr->*u;
}
reference operator+=(difference_type diff)
{ _ptr += diff; return (*this); }
reference operator-=(difference_type diff)
{ _ptr -= diff; return (*this); }
};
template<class T, class U>
static inline bool operator==(const pointer<T>& p1, const pointer<U>& p2) noexcept
{
return p1.get() == p2.get();
}
// この減算は例の厄介なやつ。
template<class T, class U>
static inline auto operator-(const pointer<T>& p1, const pointer<U>& p2)
->typename pointer<T>::difference_type
{
return typename pointer<T>::difference_type(p1.get() - p2.get());
}
9/21/2016 追記
pointerのコードをちゃんと見直して、コンパイラに通してみました。また、見栄張ってメンバへのポインタ演算子を実装しました。ガッツリC++14の機能を使っています。
以下のURLで実際に動くところを見ることができます。
http://ideone.com/WZ6vaB
おわりに
長くなりましたが以上で全てのオーバーロード可能な演算子を解説いたしました。
大げさなタイトルでごめんなさい。
誤りや不明な点があればコメントにお願いします。