#24(の2) すべての引数に型変換が必要なら,非メンバ関数を定義する.

ということで前回エントリの続きです.

演算子の定義

前回整数値からRationalへの暗黙の型変換をするには,コンストラクタが使われていることを実験しました.今回はもう一歩進んで,掛け算の演算子オペレータを定義したいと思います.早速サンプルコードですが,

#include <iostream>
#include <string>

class Rational {
public:
  Rational(int numerator=0, int denominator=1) 
   : m_numerator(numerator), m_denominator(denominator) 
  { std::cout << "Rational(int numerator, int denominator)" << std::endl; }

  ~Rational()
  { std::cout << "~Rational()" << std::endl; }

  Rational(const Rational &rhs) {
    this->m_numerator = rhs.m_numerator;
    this->m_denominator = rhs.m_denominator;
    std::cout << "Rational(const Rational &rhs)" << std::endl;
  }

  Rational& operator=(const Rational& rhs) {
    std::cout << "operator=(const Rational& rhs)" << std::endl;
    m_numerator = rhs.m_numerator;
    m_denominator = rhs.m_denominator;
    return *this;
  }

  Rational operator*(const Rational& rhs) {
    std::cout << "operator*(const Rational& rhs)" << std::endl;
    return Rational(this->m_numerator * rhs.m_numerator, this->m_denominator * rhs.m_denominator);
  }

  void say() {
    std::cout << "m_numerator : " << m_numerator << std::endl;
    std::cout << "m_denominator : " << m_denominator << std::endl;    
  }

  int m_numerator;
  int m_denominator;
};

int main(int, char**) {
  std::cout << "chap_4th_24_3.cpp" << std::endl;

  // 100からの暗黙の型変換.
  Rational r = 100;

  // 2からの暗黙の型変換 + 掛け算の実行.
  Rational ans = r * 2;
  Rational ans2 = r.operator*(Rational(2));

  ans.say();
  ans2.say();

  return 0;
}

上記の実行結果は下記のようになります.

chap_4th_24_3.cpp
Rational(int numerator, int denominator)
Rational(int numerator, int denominator)
operator*(const Rational& rhs)
Rational(int numerator, int denominator)
~Rational()
Rational(int numerator, int denominator)
operator*(const Rational& rhs)
Rational(int numerator, int denominator)
~Rational()
m_numerator : 200
m_denominator : 1
m_numerator : 200
m_denominator : 1
~Rational()
~Rational()
~Rational()

ということで,掛け算が演算子 "*" で無事に実行されることがわかりましたが,関数呼び出しを追いかけてみると掛け算の一行で複数の関数が呼ばれていることがわかります.

1.r * 2 の ”2” の Rational への暗黙の型変換のためのコンストラクタ呼び出し.
2.r * 2 の ”*” のための掛け算オペレータ呼び出し.

また,"r * 2" の演算は実際には下記の関数呼び出しと等価であることがわかります.(し,実際にこう書いてもコンパイル&実行できます.)
r.operator*(Rational(2))

上記の様に書けば結局は自分がクラスの中に定義した演算子オペレータとコンストラクタが呼び出されていることがはっきりと認識できます.で,Effective C++はもう一歩進んで,掛け算の順序入れ替えの話を持ち出してきます.数学的には,下記の演算は同じになるはずです.
"r * 2 と 2 * r"
では,下記のコードも実行できるかというと....

int main(int, char**) {
  std::cout << "chap_4th_24_3.cpp" << std::endl;

  // 100からの暗黙の型変換.
  Rational r = 100;

  // 順番入れ替え
  Rational ans3 = 2 * r;
  Rational ans4 = 2.operator*(r);

  ans.say();
  ans2.say();

  return 0;
}

当然のことながらコンパイルエラーになります.これも,明示的にすべてを書き出してみると明らかで,"2.operator*(r)" なんて関数がどこにも定義されていないことことが原因です.というわけで,Rationalのオブジェクト2つを引数に取るメソッド(演算子オペレータ)の定義が必要になります.

#include <iostream>
#include <string>

class Rational {
public:
  Rational(int numerator=0, int denominator=1) 
   : m_numerator(numerator), m_denominator(denominator) 
  { std::cout << "Rational(int numerator, int denominator)" << std::endl; }

  ~Rational()
  { std::cout << "~Rational()" << std::endl; }

  Rational(const Rational &rhs) {
    this->m_numerator = rhs.m_numerator;
    this->m_denominator = rhs.m_denominator;
    std::cout << "Rational(const Rational &rhs)" << std::endl;
  }

  Rational& operator=(const Rational& rhs) {
    std::cout << "operator=(const Rational& rhs)" << std::endl;
    m_numerator = rhs.m_numerator;
    m_denominator = rhs.m_denominator;
    return *this;
  }

  void say() {
    std::cout << "m_numerator : " << m_numerator << std::endl;
    std::cout << "m_denominator : " << m_denominator << std::endl;    
  }

  int m_numerator;
  int m_denominator;
};

const Rational operator*(const Rational &lhs, const Rational &rhs) {
  std::cout << "operator*(const Rational &lhs, const Rational &rhs)" << std::endl;
  return Rational(lhs.m_numerator * rhs.m_numerator, rhs.m_denominator * rhs.m_denominator);
}

int main(int, char**) {
  std::cout << "chap_4th_24_3.cpp" << std::endl;

  // 100からの暗黙の型変換.
  Rational r = 100;
  
  Rational ans1 = r * 2;
  Rational ans2 = operator*(r, 2);

  // 順番入れ替え
  Rational ans3 = 2 * r;
  Rational ans4 = operator*(2, r);

  ans3.say();
  ans4.say();

  return 0;
}

上記コードの実装では,掛け算の順番入れ替えにも対応できました.一点注目してほしい箇所としては,前までのサンプルコードにあった,Rationalクラス中の演算子オペレータを削除してます.なぜかというと,クラス外に定義した非メンバ関数で両方対応できるからです.最終的に,これが24項の主張している"thisを含めたすべての引数に型変換が必要なら,メンバでない関数を宣言しよう"となります.