C++で複素数演算用クラスを作る

講義の演習課題メモとして残しておきます。

詰まったのが、返り値を統一初期化記法で返す時に、『コピーコンストラクタ→コンストラクタ』で呼び出されるということ...。

コピーコンストラクタが呼び出されたら、コンストラクタ呼び出されないと思ってた...。

#include <iostream>
#include <stdio.h>
#include <math.h>

using namespace std;

class Complex
{
private:
    double re ;
    double im ;
public:
    // ========== コンストラクタ ========== //
    Complex( const double r, const double i) : re(r), im(i) {}

    // コピーコンストラクタ
    Complex(const Complex &obj) = default;

    Complex() : re(0), im(0) {}

    // ========== デストラクタ ========== //
    ~Complex() = default;

    // ========== 関数群 ========== //
    // 表示
    void print()
    {
        if (im >= 0) {
            printf("%lf + j%lf\n", re, im);
        }
        else
        {
            printf("%lf - j%lf\n", re, abs(im));
        }
    }

    // 実部
    double get_re() { return re; }

    // 虚部
    double get_im() { return im; }

    // 絶対値
    double get_abs() { return sqrt(re * re + im * im); }

    // ========== 演算子オーバーロード ========== //
    // 加算
    Complex operator + (Complex num)
    {
        double _re = this->re + num.re;
        double _im = this->im + num.im;
        return {_re, _im};
    }

    // 減算
    Complex operator - (Complex num)
    {
        double _re = this->re - num.re;
        double _im = this->im - num.im;
        return {_re, _im};
    }

    // 乗算
    Complex operator * (Complex num)
    {
        double _re = this->re * num.re - this->im * num.im;
        double _im = this->re * num.im + this->im * num.re;
        return {_re, _im};
    }

    // 除算
    Complex operator / (Complex num)
    {
        double _re = (this->re * num.re + this->im * num.im) / num.get_abs();
        double _im = (this->re * num.im - this->im * num.re) / num.get_abs();
        return {_re, _im};
    }
} ;

class __Complex
{
private:
    double r ;
    double th ;
public:
    // ========== コンストラクタ ========== //
    __Complex(const double re, const double im)
    {
        r = sqrt(re * re + im * im);
        th = atan2(im, re);
    }

    // コピーコンストラクタ
    __Complex(const __Complex &obj)
    {
        r = obj.r;
        th = obj.th;
    }

    __Complex() : r(0), th(0) {}

    // =========== デストラクタ ============ //
    ~__Complex() = default;

    // ========== 関数群 ========== //
    // 表示
    void print()
    {
        printf("%lf%lf\n", r, th);
    }

    // 実部
    double get_re() { return r * cos(th); }

    // 虚部
    double get_im() { return r * sin(th); }

    // 絶対値
    double get_abs() { return r; }

    // 偏角
    double get_arg() { return th; }

    // 極座標系 → 直交座標系
    Complex to_cart()
    {
        double _re = r * cos(th);
        double _im = r * sin(th);
        return {_re, _im};
    }

    // ========== 演算子オーバーロード ========== //
    // 加算
    __Complex operator + (__Complex num)
    {
        Complex _left = this->to_cart();
        Complex _right = num.to_cart();
        Complex _sum = _left + _right;

        return {_sum.get_re(), _sum.get_im()};
    }

    // 減算
    __Complex operator - (__Complex num)
    {
        Complex _left = this->to_cart();
        Complex _right = num.to_cart();
        Complex _sub = _left - _right;
        return {_sub.get_re(), _sub.get_im()};
    }

    // 乗算
    __Complex operator * (__Complex num)
    {
        double _r = this->get_abs() * num.get_abs();
        double _th = this->th + num.th;
        __Complex _temp;
        _temp.r = _r;
        _temp.th = _th;
        return _temp;
    }

    // 除算
    __Complex operator / (__Complex num)
    {
        Complex _left = this->to_cart();
        Complex _right = num.to_cart();
        Complex _div = _left / _right;

        return {_div.get_re(), _div.get_im()};
    }
} ;

int main ()
{
    // ====================================================
    printf("==================== 直交座標クラス ======================\n");
    // z = √3 + j
    Complex z(sqrt(3), 1);
    printf("z = √3 + j                  : ");
    z.print();              // 1.732051 + j1.000000

    // za = 0.5 * (√3 + j)
    Complex za(sqrt(3)/2, 1.0/2);
    printf("za = 0.5 * (√3 + j)         : ");
    za.print();             // 0.866025 + j0.500000

    // zb = 1 + j√3
    Complex zb = z * za;
    printf("zb = z * za = 1 + j√3       : ");
    zb.print();             // 1.000000 + j1.732051

    // zz = -√3 - j
    Complex zz(-sqrt(3), -1);
    printf("zz = -√3 - j                : ");
    zz.print();             // -1.732051 - j1.000000

    // zza = -0.5 * (√3 + j)
    Complex zza(-sqrt(3)/2, -1.0/2);
    printf("zza = -0.5 * (√3 + j)       : ");
    zza.print();            // -0.866025 - j0.500000

    // zzb = 1 + j√3
    Complex zzb = zz * zza;
    printf("zzb = zz * zza = 1 + j√3    : ");
    zzb.print();            // 1.000000 + j1.732051

    printf("=======================================================\n");
    // ====================================================

    // ====================================================
    printf("====================== 極座標クラス =====================\n");

    // z1 = 2∠(pi/6)
    // z1 = 2{cos(pi/6) + j * sin(pi/6)}
    __Complex z1(sqrt(3), 1);
    printf("z1 = 2∠(π/6)                : ");
    z1.print();             // 2.000000 ∠ 0.523599

    // z2 = 1∠(pi/6)
    // z2 = cos(pi/6) + j * sin(pi/6)
    __Complex z2(sqrt(3)/2, 1.0/2);
    printf("z2 = 1∠(π/6)                : ");
    z2.print();             // 1.000000 ∠ 0.523599

    // z3 = 2∠(pi/3)
    __Complex z3 = z1 * z2;
    printf("z3 = z1 * z2 = 2∠(pi/3)     : ");
    z3.print();             // 2.000000 ∠ 1.047198
    printf("z3 = z1 * z2 = 1 + j√3      : ");
    z3.to_cart().print();   // 1.000000 + j1.732051

    // zz1 = 2∠(-pi/6)
    __Complex zz1(-sqrt(3), -1);
    printf("zz1 = -√3 - j               : ");
    zz1.print();            // 2.000000 ∠ -2.617994

    // zz2 = = 1∠(-pi/6)
    __Complex zz2(-sqrt(3)/2, -1.0/2);
    printf("zz2 = -0.5 * (√3 + j)       : ");
    zz2.print();            // 1.000000 ∠ -2.617994

    // zz3 = 2∠(-pi/3)
    __Complex zz3 = zz1 * zz2;
    printf("zz3 = zz1 * zz2 = 2∠(-pi/3) : ");
    zz3.print();            // 2.000000 ∠ -5.235988
    printf("zz3 = zz1 * zz2 = 1 + j√3   : ");
    zz3.to_cart().print();  // 1.000000 + j1.732051
    printf("=======================================================\n");
    // ====================================================

    return 0;
}

考察点

極座標クラスで(1)のように演算子をオーバーライドしてみた。

// (1)
// 乗算
__Complex operator * (__Complex num)
{
    double _r = this->get_abs() * num.get_abs();
    double _th = this->th + num.th;
    return {_r, _th};
}

この状態で(2)のような計算を行う。

// (2)
__Complex z1(sqrt(3), 1);        // z1 = 2∠(pi/6)
__Complex z2(sqrt(3)/2, 1.0/2);  // z2 = 1∠(pi/6)
__Complex z3 = z1 * z2;          // z3 = 2∠(pi/3)

この計算が正常に行われているならば、z3を出力すると

// (3)
z3.print();            // 2.000000 ∠ 1.047198
z3.to_cart().print();    // 1.000000 + j1.732051

(3)のように表示されなければならない。しかし、(1)のように定義した状態では(4)のような出力結果となる

// (4)
z3.print();            // 2.257570 ∠ 0.482348
z3.to_cart().print();    // 2.000000 + j1.047198

この原因が、演算子オーバーライドメソッドでの返り値の返し方に問題があることが分かった。(1)のreturn文はC++11で新たに追加された『統一初期化記法(Uniform Initialization)』を利用している。統一初期化記法を用いてreturnする場合、返り値クラスで初期化されたオブジェクトが returnされる。(この場合、__Complex) つまり、『r, th』で計算済みのオブジェクトを統一初期化記法でreturn すると、__Complexのコンストラクタによって、再計算されたオブジェクトがreturn されることになる。そのため演算子オーバーライドメソッド内に局所変数でreturnオブジェクトを作成することで回避した。

// (5)
// 乗算
__Complex operator * (__Complex num)
{
    double _r = this->get_abs() * num.get_abs();
    double _th = this->th + num.th;
    __Complex _temp;
    _temp.r = _r; _temp.th = _th;
    return _temp;
}

統一初期化記法(Uniform Initialization, Universal Initialization)

C++11から、変数、配列、構造体、STLコンテナに関わらず、同じように初期化できるようになった。この{…}を使った統一的な初期化表現を『Uniform Initialization』または『Universal Initialization』と呼ぶ。この初期化方法はC++を開発したBjarne Stroustrup(ビャーネ・ストロヴストルップ)氏も推奨している。

// == 従来の書き方 ==
// "=" を使って
int x = 3;

// "= {}" を使って
int a[] = { 0, 1, 2, 3 };

// これも "= {}" を使って
struct S1 {
  int a, b;
} s = { 0, 1 }; 

// ループを使って。
std::vector<int> v;
for(int i = 0; i < 4; ++i) v.push_back(a[i]);
// == 統一的な書き方 ==
int x { 3 };
int a[] { 0, 1, 2, 3 };
struct S1 {
  int a, b;
} s { 0, 1 };
std::vector<int> v { 0, 1, 2, 3 };

しかし、コンストラクタにexplicitがあるクラスでは、暗黙キャストとして扱われる統一初期化はコンパイルエラーとなり使用できない。

tuple<int, char> createTuple(void)
{
  return { 1, 'a' };  // Compile error
}

explicit

『explicit指定子』とは、コンストラクタが明示的であること(暗黙の変換、コピー初期化が使用できないこと)を指定する指定子。

struct A {
    A(int) { }      // コンストラクタ。
    A(int, int) { } // コンストラクタ (C++11)。
    operator bool() const { return true; }
};
 
struct B {
    explicit B(int) { }
    explicit B(int, int) { }
    explicit operator bool() const { return true; }
};

A a1 = 1;      // OK、コピー初期化は A::A(int) を選択します。
B b1 = 1;      // エラー、コピー初期化は B::B(int) を考慮しません。

A a4 = {4, 5}; // OK、コピーリスト初期化は A::A(int, int) を選択します。
B b4 = {4, 5}; // エラー、コピーリスト初期化は B::B(int,int) を考慮しません。

bool na1 = a1; // OK、コピー初期化は A::operator bool() を選択します。
bool nb1 = b2; // エラー、コピー初期化は B::operator bool() を考慮しません。

f:id:nomunomu0504:20190411151221p:plain:w0