Vec2 クラスを作る(モダンな C++ クラスデザインのためのチュートリアル)

  • 21
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

初心者 C++er Advent Calendar 2015 5 日目の記事です。
C++ のクラスデザインの入門として、二次元ベクトルを表現するクラスの作り方を解説します。

0. はじめに

地図上に 2 匹の動物 cat と dog がいます。
この 2 匹がどこにいるのか位置(座標)を決めて、2 匹の間の距離を計算しましょう。

クラスを使って書くとこうなります。

const Vec2 catPos(3.2, 5.4);
const Vec2 dogPos(4.0, 7.5);
std::cout << "cat: " << catPos << '\n';
std::cout << "dog: " << dogPos << '\n';
std::cout << "distance: " << catPos.distanceFrom(dogPos) << '\n';
実行結果
cat: (3.2,5.4)
dog: (4,7.5)
distance: 2.24722

クラスを使わないで書くとこうなります。

const double catPosX = 3.2, catPosY = 5.4;
const double dogPosX = 4.0, dogPosY = 7.5;
std::cout << "cat: (" << catPosX << ',' << catPosY << ")\n";
std::cout << "dog: (" << dogPosX << ',' << dogPosY << ")\n";
std::cout << "distance: " << std::sqrt((dogPosX-catPosX)*(dogPosX-catPosX) + (dogPosY-catPosY)*(dogPosY-catPosY)) << '\n';
実行結果
cat: (3.2,5.4)
dog: (4,7.5)
distance: 2.24722

適切なクラスを設計すれば、見通しの良いプログラムで問題解決ができるようになります。
この記事では二次元ベクトルを表現するクラスを作りながら、モダンな C++ におけるクラス設計の基本を学びます。

1. 二次元ベクトル

二次元ベクトルは、2 つの成分(x 成分と y 成分)で表されるベクトルです。
例えば、2D アクションゲームのキャラクターが画面のどこにいるかといった「位置」や、ビリヤードの球がどの方向にどれぐらいの速さで移動しているのかという「速度」が二次元ベクトルで表現されます。
それぞれの成分は、キャラクターの位置の場合は x 座標と y 座標、ビリヤードの球の速度の場合は x 軸方向の速度、y 軸方向の速度を示します。

2. クラスの名前を決めよう

二次元ベクトル (2D-Vector) を扱うクラスなので、名前は Vec2 としましょう。
Vector2D でも OK です。V2 では省略しすぎて「バージョン 2」と紛らわしいので避けるべきです。Vector はベクトルの次元がわからないので好ましくありません。

今回は扱いませんが、三次元ベクトルクラスを Vec3, 四次元ベクトルクラスを Vec4 と名付けることで一貫性を得られます。

3. class にすべきか struct にすべきか

classstruct は、クラスの外から メンバ にアクセスできるかを決める アクセス制御 のデフォルトの設定が異なります。class はすべてのメンバがデフォルトで private であり、struct はすべてのメンバがデフォルトで public です。それ以外は基本的に同じで、実行時の性能やコストに違いはありません。この記事では両方をまとめて「クラス」といいます。

Vec2 クラスの場合、クラスの外からのアクセスを制限する必要があるメンバ変数や関数はありません。したがって、デフォルトのアクセス制御が public である struct を使いましょう。

struct Vec2
{
    double x;
    double y;
};

4. コンストラクタを作ろう

Vec2 v(10, 20); のように、2 つの成分の値から Vec2 型のオブジェクトを作れるようにします。
統一初期化構文 を用いれば、何もしなくても Vec2 v{10, 20}; と書くことができますが、初期化の方法を複数用意したり、カスタマイズしたりしたい場合には コンストラクタ を使います。

まずは x, y 2 つの値から Vec2 型のオブジェクトを構築するコンストラクタを作りましょう。

struct Vec2
{
    double x;
    double y;

    Vec2(double _x, double _y) // コンストラクタ
        : x(_x)
        , y(_y) {}
};

次に、Vec2 v; のように、初期値を与えない場合のための デフォルトコンストラクタ も作りましょう。
自分でコンストラクタを定義することもできますが、= default とすることで、コンパイラによる デフォルトの初期化 を利用できます。このときメンバ変数 x, y は未初期化のままです。

struct Vec2
{
    double x;
    double y;

    Vec2() = default; // デフォルトコンストラクタ

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}
};

Vec2 のデフォルトコンストラクタでは、値の未初期化を避けるために各成分を 0 で初期化するのも良いアイデアです。

    Vec2()
        : x(0.0)
        , y(0.0) {}

しかし、その分実行時にコストが発生することに注意してください。
例えば std::vector<Vec2> v(1000); のように Vec2 型のオブジェクトを大量に作成するコードで、すべての要素の成分を 0 で初期化する処理が必要になります。

この程度の処理であればコンパイラの最適化によって、無視できる程度のコストに抑えられるかもしれませんが、最大限の実行時性能を求めるプログラマからは不満が出る設計になってしまうでしょう。どちらを採用するかは目的に応じて判断しましょう。

5. メンバ関数を作ろう

ベクトルの長さを計算する関数を フリー関数(非メンバ関数)で定義すると次のようになります。

double Length(const Vec2& v)
{
    return std::sqrt(v.x * v.x + v.y * v.y);
}

int main()
{
    Vec2 v(10, 20);

    std::cout << Length(v) << '\n';
}

一方、メンバ関数 では、自身のオブジェクトを明示的に示さずに、メンバの名前をそのまま使えます。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() // ベクトルの長さを計算するメンバ関数
    {
        return std::sqrt(x * x + y * y);
    }
};

int main()
{
    Vec2 v(10, 20);

    std::cout << v.length() << '\n';
}

メンバ関数が自身のクラスのメンバ変数を変更しない場合は、関数の引数の並びの後に const をつけます。const をつけることでメンバ関数は const メンバ関数 になり、const オブジェクトに対しても実行できるようになります。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const // ベクトルの長さを計算する const メンバ関数
    {
        return std::sqrt(x * x + y * y);
    }
};

int main()
{
    const Vec2 v(10, 20);

    std::cout << v.length() << '\n'; // length() が const メンバ関数でないとコンパイルエラー
}

練習のために、ベクトルの計算で役に立ちそうなメンバ関数をいくつか実装してみましょう。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(x * x + y * y);
    }

    double lengthSquare() const // ベクトルの長さの二乗
    {
        return x * x + y * y;
    }

    double dot(const Vec2& other) const // もう一方のベクトルとの内積
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const    // もう一方のベクトルとの距離
    {
        return std::sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y));
    }

    Vec2 normalized() const // 正規化(長さを1にした)ベクトル
    {
        return{ x / length() , y / length() };
    }

    bool isZero() const // ゼロベクトルであるか
    {
        return x == 0.0 && y == 0.0;
    }
};

6. 演算子をオーバーロードしよう

【Q】: 位置 Vec2(10, 20) にいる人が、毎秒 Vec2(3, 2) の速度で移動したとき、3 秒後にはどこにいるでしょう?

― こんな問題は次のような式で計算するのが簡単です。

const Vec2 answer = Vec2(10, 20) + 3 * Vec2(3, 2);

自分で定義したクラスが +- などの 演算子 を使えるようにするには、クラスのオブジェクトに対する演算子の使い方とその処理を定義する必要があります。これを 演算子のオーバーロード といいます。

まずはじめに単純な +- をオーバーロードしましょう。
基本的な文法はメンバ関数と同じです。
関数名の代わりに operator ■() とすることで、Vec2 型の値 v に対する演算子 ■v を定義できます。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(x * x + y * y);
    }

    double lengthSquare() const
    {
        return x * x + y * y;
    }

    double dot(const Vec2& other) const
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const
    {
        return std::sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y));
    }

    Vec2 normalized() const
    {
        return{ x / length() , y / length() };
    }

    bool isZero() const
    {
        return x == 0.0 && y == 0.0;
    }

    Vec2 operator +() const // 単項 +
    {
        return *this;
    }

    Vec2 operator -() const // 単項 -
    {
        return{ -x, -y };
    }
};

int main()
{
    const Vec2 a(1, 2);

    const Vec2 b = +a; // (1, 2)

    const Vec2 c = -a; // (-1, -2)
}

operator ■(other) で、Vec2 型の値 v に対する演算 v ■ other を定義できます。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(x * x + y * y);
    }

    double lengthSquare() const
    {
        return x * x + y * y;
    }

    double dot(const Vec2& other) const
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const
    {
        return std::sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y));
    }

    Vec2 normalized() const
    {
        return{ x / length() , y / length() };
    }

    bool isZero() const
    {
        return x == 0.0 && y == 0.0;
    }

    Vec2 operator +() const
    {
        return *this;
    }

    Vec2 operator -() const
    {
        return{ -x, -y };
    }

    Vec2 operator +(const Vec2& other) const // 2項 +
    {
        return{ x + other.x, y + other.y };
    }

    Vec2 operator -(const Vec2& other) const // 2項 -
    {
        return{ x - other.x, y - other.y };
    }

    Vec2 operator *(double s) const // 2項 *
    {
        return{ x * s, y * s };
    }

    Vec2 operator /(double s) const // 2項 /
    {
        return{ x / s, y / s };
    }
};

int main()
{
    const Vec2 a(1, 2), b(2, 4);

    const Vec2 c = a + b; // (3, 6)

    const Vec2 d = a - b; // (-1, -2)

    const Vec2 e = a * 3; // (3, 6)

    const Vec2 f = b / 2; // (0.5, 1)
}

複合代入演算は自身のメンバ変数を変更するため、const をつけることはできません。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(x * x + y * y);
    }

    double lengthSquare() const
    {
        return x * x + y * y;
    }

    double dot(const Vec2& other) const
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const
    {
        return std::sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y));
    }

    Vec2 normalized() const
    {
        return{ x / length() , y / length() };
    }

    bool isZero() const
    {
        return x == 0.0 && y == 0.0;
    }

    Vec2 operator +() const
    {
        return *this;
    }

    Vec2 operator -() const
    {
        return{ -x, -y };
    }

    Vec2 operator +(const Vec2& other) const
    {
        return{ x + other.x, y + other.y };
    }

    Vec2 operator -(const Vec2& other) const
    {
        return{ x - other.x, y - other.y };
    }

    Vec2 operator *(double s) const
    {
        return{ x * s, y * s };
    }

    Vec2 operator /(double s) const
    {
        return{ x / s, y / s };
    }

    Vec2& operator +=(const Vec2& other) // 複合代入演算 +=
    {
        x += other.x;
        y += other.y;
        return *this;
    }

    Vec2& operator -=(const Vec2& other) // 複合代入演算 -=
    {
        x -= other.x;
        y -= other.y;
        return *this;
    }

    Vec2& operator *=(double s) // 複合代入演算 *=
    {
        x *= s;
        y *= s;
        return *this;
    }

    Vec2& operator /=(double s) // 複合代入演算 /=
    {
        x /= s;
        y /= s;
        return *this;
    }
};

int main()
{
    Vec2 a(10, 20);

    a += Vec2(1, 2); // (11, 22)

    a -= Vec2(1, 2); // (10, 20)

    a *= 4; // (40, 80)

    a /= 2; // (20, 40)
}

Vec2 型の値 v が後にくる演算 other ■ v は、フリー関数としてクラスの外で operator ■(other, v) のように定義します。
メンバ関数ではないので、const はつきません。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(x * x + y * y);
    }

    double lengthSquare() const
    {
        return x * x + y * y;
    }

    double dot(const Vec2& other) const
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const
    {
        return std::sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y));
    }

    Vec2 normalized() const
    {
        return{ x / length() , y / length() };
    }

    bool isZero() const
    {
        return x == 0.0 && y == 0.0;
    }

    Vec2 operator +() const
    {
        return *this;
    }

    Vec2 operator -() const
    {
        return{ -x, -y };
    }

    Vec2 operator +(const Vec2& other) const
    {
        return{ x + other.x, y + other.y };
    }

    Vec2 operator -(const Vec2& other) const
    {
        return{ x - other.x, y - other.y };
    }

    Vec2 operator *(double s) const
    {
        return{ x * s, y * s };
    }

    Vec2 operator /(double s) const
    {
        return{ x / s, y / s };
    }

    Vec2& operator +=(const Vec2& other)
    {
        x += other.x;
        y += other.y;
        return *this;
    }

    Vec2& operator -=(const Vec2& other)
    {
        x -= other.x;
        y -= other.y;
        return *this;
    }

    Vec2& operator *=(double s)
    {
        x *= s;
        y *= s;
        return *this;
    }

    Vec2& operator /=(double s)
    {
        x /= s;
        y /= s;
        return *this;
    }
};

inline Vec2 operator *(double s, const Vec2& v) // Vec2 が後にくる 2項 *
{
    return{ s*v.x, s*v.y };
}

int main()
{
    const Vec2 a(10, 20);

    const Vec2 b = 3 * a; // (30, 60)
}

7. 入出力演算子をオーバーロードしよう

Vec2 の値を標準入力・標準出力するたびにこんなコードを書いていては手間がかかります。

Vec2 v;
char c;
std::cin >> c >> v.x >> c >> v.y >> c; // (10,20) と入力すると

v *= 2;

std::cout << '(' << v.x << ',' << v.y << ')'; // (20,40) と出力される

入出力ストリームに対する <<, >> 演算子もオーバーロードしてしまいましょう。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(x * x + y * y);
    }

    double lengthSquare() const
    {
        return x * x + y * y;
    }

    double dot(const Vec2& other) const
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const
    {
        return std::sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y));
    }

    Vec2 normalized() const
    {
        return{ x / length() , y / length() };
    }

    bool isZero() const
    {
        return x == 0.0 && y == 0.0;
    }

    Vec2 operator +() const
    {
        return *this;
    }

    Vec2 operator -() const
    {
        return{ -x, -y };
    }

    Vec2 operator +(const Vec2& other) const
    {
        return{ x + other.x, y + other.y };
    }

    Vec2 operator -(const Vec2& other) const
    {
        return{ x - other.x, y - other.y };
    }

    Vec2 operator *(double s) const
    {
        return{ x * s, y * s };
    }

    Vec2 operator /(double s) const
    {
        return{ x / s, y / s };
    }

    Vec2& operator +=(const Vec2& other)
    {
        x += other.x;
        y += other.y;
        return *this;
    }

    Vec2& operator -=(const Vec2& other)
    {
        x -= other.x;
        y -= other.y;
        return *this;
    }

    Vec2& operator *=(double s)
    {
        x *= s;
        y *= s;
        return *this;
    }

    Vec2& operator /=(double s)
    {
        x /= s;
        y /= s;
        return *this;
    }
};

inline Vec2 operator *(double s, const Vec2& v)
{
    return{ s*v.x, s*v.y };
}

template <class Char> // 出力ストリーム
inline std::basic_ostream<Char>& operator <<(std::basic_ostream<Char>& os, const Vec2& v)
{
    return os << Char('(') << v.x << Char(',') << v.y << Char(')');
}

template <class Char> // 入力ストリーム
inline std::basic_istream<Char>& operator >>(std::basic_istream<Char>& is, Vec2& v)
{
    Char unused;
    return is >> unused >> v.x >> unused >> v.y >> unused;
}

int main()
{
    Vec2 v;

    std::cin >> v; // (10,20) と入力すると

    std::cout << 2 * v << '\n'; // (20,40) と出力される
}

サンプルコードでは示していませんが、basic_ostreambasic_istream に対する <<, >> 演算子をオーバーロードすることで、それらを継承する fstreamstringstream でも Vec2 を扱えるようになりました。

8. Don't Repeat Yourself

クラスを見渡すと、何箇所か同じコードを書いている冗長な部分が見つかります。
プログラムのバグを減らすにはコードを短くすることが大切です。別のメンバ関数で置き換えられる部分がないか見直してみましょう。

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(lengthSquare()); // std::sqrt(x * x + y * y)
    }

    double lengthSquare() const
    {
        return dot(*this); // x * x + y * y
    }

    double dot(const Vec2& other) const
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const
    {
        return (other - *this).length(); //  std::sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y));
    }

    Vec2 normalized() const
    {
        return *this / length(); // { x / length() , y / length() }
    }

    bool isZero() const
    {
        return x == 0.0 && y == 0.0;
    }

    Vec2 operator +() const
    {
        return *this;
    }

    Vec2 operator -() const
    {
        return{ -x, -y };
    }

    Vec2 operator +(const Vec2& other) const
    {
        return{ x + other.x, y + other.y };
    }

    Vec2 operator -(const Vec2& other) const
    {
        return{ x - other.x, y - other.y };
    }

    Vec2 operator *(double s) const
    {
        return{ x * s, y * s };
    }

    Vec2 operator /(double s) const
    {
        return{ x / s, y / s };
    }

    Vec2& operator +=(const Vec2& other)
    {
        x += other.x;
        y += other.y;
        return *this;
    }

    Vec2& operator -=(const Vec2& other)
    {
        x -= other.x;
        y -= other.y;
        return *this;
    }

    Vec2& operator *=(double s)
    {
        x *= s;
        y *= s;
        return *this;
    }

    Vec2& operator /=(double s)
    {
        x /= s;
        y /= s;
        return *this;
    }
};

inline Vec2 operator *(double s, const Vec2& v)
{
    return{ s*v.x, s*v.y };
}

template <class Char>
inline std::basic_ostream<Char>& operator <<(std::basic_ostream<Char>& os, const Vec2& v)
{
    return os << Char('(') << v.x << Char(',') << v.y << Char(')');
}

template <class Char>
inline std::basic_istream<Char>& operator >>(std::basic_istream<Char>& is, Vec2& v)
{
    Char unused;
    return is >> unused >> v.x >> unused >> v.y >> unused;
}

9. constexpr

最後の仕上げとして、メンバ関数を constexpr (コンスタント・エクスプレッション) にしましょう。
constexpr をつけた関数の処理は、もし可能であればコンパイルの時点で計算されるようになり、実行時の計算コストを減らせます。

constexpr によって

std::cout << (Vec2(10, 20) + Vec2(5, 5)).lengthSquare();

といった計算はコンパイルの時点で

std::cout << 850;

というコードになり、実行時に計算するコストはゼロになります。

メンバ関数を constexpr 修飾するには、その関数で使用する関数が全て constexpr である必要があるなど、いくつかの制約 があります。例えば、Vec2::lengthSquare()constexpr にできますが、Vec2::length()constexpr でない std::sqrt() を使うため constexpr にできません。

可能なメンバ関数すべてを constexpr にして、Vec2 クラスを完成させましょう。

10. 完成

struct Vec2
{
    double x;
    double y;

    Vec2() = default;

    constexpr Vec2(double _x, double _y)
        : x(_x)
        , y(_y) {}

    double length() const
    {
        return std::sqrt(lengthSquare());
    }

    constexpr double lengthSquare() const
    {
        return dot(*this);
    }

    constexpr double dot(const Vec2& other) const
    {
        return x * other.x + y * other.y;
    }

    double distanceFrom(const Vec2& other) const
    {
        return (other - *this).length();
    }

    Vec2 normalized() const
    {
        return *this / length();
    }

    constexpr bool isZero() const
    {
        return x == 0.0 && y == 0.0;
    }

    constexpr Vec2 operator +() const
    {
        return *this;
    }

    constexpr Vec2 operator -() const
    {
        return{ -x, -y };
    }

    constexpr Vec2 operator +(const Vec2& other) const
    {
        return{ x + other.x, y + other.y };
    }

    constexpr Vec2 operator -(const Vec2& other) const
    {
        return{ x - other.x, y - other.y };
    }

    constexpr Vec2 operator *(double s) const
    {
        return{ x * s, y * s };
    }

    constexpr Vec2 operator /(double s) const
    {
        return{ x / s, y / s };
    }

    Vec2& operator +=(const Vec2& other)
    {
        x += other.x;
        y += other.y;
        return *this;
    }

    Vec2& operator -=(const Vec2& other)
    {
        x -= other.x;
        y -= other.y;
        return *this;
    }

    Vec2& operator *=(double s)
    {
        x *= s;
        y *= s;
        return *this;
    }

    Vec2& operator /=(double s)
    {
        x /= s;
        y /= s;
        return *this;
    }
};

inline constexpr Vec2 operator *(double s, const Vec2& v)
{
    return{ s*v.x, s*v.y };
}

template <class Char>
inline std::basic_ostream<Char>& operator <<(std::basic_ostream<Char>& os, const Vec2& v)
{
    return os << Char('(') << v.x << Char(',') << v.y << Char(')');
}

template <class Char>
inline std::basic_istream<Char>& operator >>(std::basic_istream<Char>& is, Vec2& v)
{
    Char unused;
    return is >> unused >> v.x >> unused >> v.y >> unused;
}

▲ コードを編集して実行してみる
上記の URL では、このコードを実際に編集して実行結果を確かめることができます。


この記事に登場する C++ コードはすべてパブリックドメインとします。