6. 演算子オーバーロードの基本(基本ではないかも)
メンバー関数のオーバーロードと非メンバー関数のオーバーロード¶
演算子のオーバーロードには、メンバー関数として定義する方法と非メンバー関数として定義する方法の2つがあります。
operator->
/operator()
/operator[]
/operator=
の4つはメンバー関数として定義する必要があります。
メンバ関数として定義すると、第一引数は必ず自身のクラスのオブジェクトになります。 対して、非メンバー関数として定義すると、第一引数は自身のクラスのオブジェクトである必要はありません。 よって、テンプレートを使って汎用的に定義することができます。 ライブラリを設計する場合は、非メンバー関数として定義することが多いです。
multidimensional subscript operator (Since C++23)¶
C++23 からは operator[]
を多次元配列用にオーバーロードできるようになりました。
operator[]
の引数を複数受け取ることができます。
#include <array>
#include <cassert>
#include <iostream>
template<typename T, std::size_t Z, std::size_t Y, std::size_t X>
struct Array3d
{
std::array<T, X * Y * Z> m{};
constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23
{
assert(x < X and y < Y and z < Z);
return m[z * Y * X + y * X + x];
}
};
int main()
{
Array3d<int, 4, 3, 2> v;
v[3, 2, 1] = 42;
std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n';
}
v[3, 2, 1]
のように、 operator[]
に複数の引数を渡すことができます。
このコードは C++20 まではカンマ演算子として解釈されてしまい、意図した動作をしません。
C++20からは operator[]
に複数の引数を渡すことは規格で非推奨に指定されています。
C++20以前の挙動を期待する場合は、v[(3, 2, 1)]
のように、引数を丸括弧で囲めばできます。
演算子のオーバーロードと explicit object parameter (C++23)¶
C++23 からは、メンバー関数として定義する演算子のオーバーロードにおいても explicit object parameter (3章を参照) を使うことができるようになりました。 これはまだ利点がわからないかもしれないですが、テンプレートを使うときに威力を発揮します。
#include <iostream>
struct Test
{
int value;
constexpr Test operator+(this auto const& self, Test const& other)
{
return Test{ .value = self.value + other.value };
}
};
int main()
{
Test a{ 1 };
Test b{ 2 };
Test c = a + b;
std::cout << c.value << '\n';
}
static overloaded operators (Since C++23)¶
C++23からは this ポインタを持つ必要がない静的メンバー関数として operator()
と operator[]
をオーバーロードできるようになりました。
this ポインタが存在しないため、explicit object parameter を使うこともできません。
static call/subscript operator¶
struct SwapThem
{
template<typename T>
static void operator()(T& lhs, T& rhs)
{
std::ranges::swap(lhs, rhs);
}
template<typename T>
static void operator[](T& lhs, T& rhs)
{
std::ranges::swap(lhs, rhs);
}
};
inline constexpr SwapThem swap_them{};
void foo()
{
int a = 1, b = 2;
swap_them(a, b); // OK
swap_them[a, b]; // OK
SwapThem{}(a, b); // OK
SwapThem{}[a, b]; // OK
SwapThem::operator()(a, b); // OK
SwapThem::operator[](a, b); // OK
SwapThem(a, b); // error, invalid construction
SwapThem[a, b]; // error
}
static lambda¶
同様にラムダ式も匿名クラスであるため、静的メンバー関数として operator()
をオーバーロードできます。
ラムダキャプチャが空のラムダ式であれば、static
をつけることができます。
当然ですが this ポインタは存在しませんので mutable
指定はできません、もちろん explicit object parameter も使えません。
[]() static {}
// ^^^^^^ ここ
三方比較演算子 operator<=>
(Since C++20)¶
三方比較演算子とは¶
C++20 からは三方比較演算子 operator<=>
が導入されました。
三方比較演算子は、2つの値を比較して、等しいか、左側が小さいか、右側が大きいかを返します。
これを使うことで、すべての比較演算子の機能を1つの関数で実装できます。
そして、この演算子を定義するだけで、==
、!=
、<
、<=
、>
、>=
の6つの比較演算子が自動的に生成されます。
C++17までの比較演算子のオーバーロードは、すべての比較演算子を個別に定義する必要がありました。
#include <tuple>
struct S {
int x;
double d;
char str[4];
constexpr bool operator<(const S& rhs) const {
return std::tie(x, d, str[0], str[1], str[2], str[3])
< std::tie(rhs.x, rhs.d, rhs.str[0], rhs.str[1], rhs.str[2], rhs.str[3]);
}
constexpr bool operator==(const S& rhs) const {
return std::tie(x, d, str[0], str[1], str[2], str[3])
== std::tie(rhs.x, rhs.d, rhs.str[0], rhs.str[1], rhs.str[2], rhs.str[3]);
}
constexpr bool operator!=(const S& rhs) const {
return !(*this == rhs);
}
constexpr bool operator>(const S& rhs) const {
return rhs < *this;
}
constexpr bool operator<=(const S& rhs) const {
return !(*this > rhs);
}
constexpr bool operator>=(const S& rhs) const {
return !(*this < rhs);
}
};
C++20 からは、三方比較演算子を定義するだけで、すべての比較演算子が自動的に生成されます。
#include <compare>
struct S {
int x;
double d;
char str[4];
auto operator<=>(const S&) const = default;
};
Explicitly defaulted 定義と delete 定義¶
特殊メンバ関数と比較演算子は、default
指定と delete
指定を使うことができます。
特殊メンバ関数とは、デフォルトコンストラクタ、コピーコンストラクタ、ムーブコンストラクタ、コピー代入演算子、ムーブ代入演算子、デストラクタのことです。
比較演算子とは、operator==
、operator<=>
、operator!=
(ただし、operator==
が明示的に定義されている場合のみ)、operator<
(ただし、operator<=>
が明示的に定義されている場合のみ)、operator<=
(ただし、operator<=>
が明示的に定義されている場合のみ)、operator>
(ただし、operator<=>
が明示的に定義されている場合のみ)、operator>=
(ただし、operator<=>
が明示的に定義されている場合のみ) のことです。
まず、delete
指定について説明します。
delete
指定を使うと、その関数の呼び出しをコンパイルエラーにすることができます。
これは、すべての関数やオペレータに対して適用できます。
例えば、コピーコンストラクタを delete
指定すると、オブジェクトのコピーが禁止されます。
struct NoCopy
{
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
つぎに、default
指定について説明します。
default
指定を使うと、その関数のデフォルト実装 (自明な実装) を生成することができます。
自明な実装とはコンパイラにとって自明な実装という意味です、つまり効率的な実装が生成されます。
三方比較演算子の default
指定¶
三方比較演算子に default
指定を使うと、その型のすべてのメンバを (上から順に) 辞書式順序で比較する三方比較演算子が生成されます。
すべてのメンバが三方比較演算子をサポートしている必要があります。
しかし C++17 までに書かれたコードではすべてのクラス三方比較演算子をサポートしていないため、追加のルールがあります。
operator==
と operator<
の両方が定義されている場合は、その定義を使って operator<=>
を自動的に合成します。
理解度チェック¶
-
static
指定されたメンバー関数としての演算子オーバーロードとそうでないものを Compiler Explorer で比較してみましょう。 -
三方比較演算子を定義することで、どのような利点がありますか?