LoginSignup
7
8

More than 5 years have passed since last update.

アクセサの別名を作る

Last updated at Posted at 2017-05-24

先日、二次元座標を表現するクラスを定義しようとする記事を見ました。

C++のPointクラス

標準ライブラリにある std::complex を用いれば基本的な演算子は定義済みであるし、便利なのではないかという提案をコメントしたのですが、 std::complex はあくまで複素数を表現するクラスであるので、値を取り出そうとするとき、またはセットしようとするときのメンバ関数の名前は realimag です。 座標として考えるなら xy としてアクセスしたいという意見が出ました。

直接的にはメンバ関数の別名を定義する方法は C++ にはありません。 xy という名前でアクセスするにはどういう方法を取れるのか考えてみました。

マクロ

これはもう議論の余地もないほどに駄目なやり方ですが、可能ではある選択肢としてマクロを使う方法があります。

#define x real
#define y imag

識別子として現れる x がどこもかしこも real に置き換えられてしまうのではとんでもない問題を引き起こしかねません。 馬鹿げたエラーをいくつも引き起こすでしょう。

一般論としてはマクロは可能な限り避けるべきです。 どうしても必要であれば通常の識別子として現れそうにない名前を付けるのが望ましく、全て大文字にするという規約がしばしば採用されています。

非メンバ関数

名前が xy であることが分かりやすいのであれば必ずしもメンバ関数にしなくてもよいでしょう。 近頃はメンバ関数は最小限にするのが好ましいという風潮も一部にあり、個人的にはこれが最もバランスのとれた妥当な方法であると考えています。

point.hpp
#include <complex>

using point = std::complex<int>;

int x(point pt) { return pt.real(); }
void x(point& pt, int n) { pt.real(n); }
int y(point pt) { return pt.imag(); }
void y(point& pt, int n) { pt.imag(n); }

この例では realimag に倣ってゲッタとセッタを同じ名前にしましたが、個々のスタイルによってもっと適当な名前があるかもしれません。

残念なことに見た目がアクセサらしくなく不格好な印象はありますね。

継承

クラスの機能を拡張する基本は継承です。 std::complex<int> を継承してメンバ関数 xy を定義することは簡単にできます。

point.hpp
#include <complex>

class point : public std::complex<int> {
  using std::complex<int>::complex;
public:
  int x(void) { return real(); }
  void x(int n) { real(n); }
  int y(void) { return imag(); }
  void y(int n) { imag(n); }
};

ですが、この point はあくまでも std::complex<int> を継承したクラスであって std::complex<int> ではないので std::complex<int> として扱おうとすると問題が生じます。 致命的なのはスライシングです。

sample1.cpp
#include "point.hpp"

int main(void) {
  point* pnt = new point();

  // スライシング発生
  std::complex<int>* cpx = pnt;
  delete cpx; 
  return 0;
}

もちろんスライシングが起こらないように取り扱えばよいのですが、スライシングは多くの場合にコンパイラが捕捉できないので規模が大きくなると困ることもあるかもしれません。 こういったリスクを引き受ける価値がメンバの別名にあるのかというと疑問です。

それから、 point 型のオブジェクトに対して std::complex<int> の演算子や関数が使えますが、これらの返却値は std::complex<int> 型です。 メンバ xy を使いたければ point 型にあらためて変換しなければなりません。

継承を使うというのはあまりよい方法ではないかもしれません。

プロパティ

継承にはデメリットがあるという話は置いて、余興としてプロパティに見せかけたアクセサをやってみました。 つまり、変数に対して操作しているように見えるけれど実態はメンバ関数を呼び出しているというような定義です。

#ifndef HEADER_1d4b9c557a35772aae2a0856771ef43c
#define HEADER_1d4b9c557a35772aae2a0856771ef43c

#include <complex>
#include <functional>

class point : public std::complex<int> {
private:
  using base=std::complex<int>;
  using setter_t = void (base::*)(int);
  using getter_t = int (base::*)(void) const;

  template<setter_t setter, getter_t getter>
  class complex_property {
  private:
    using type = point;
    type* const obj;
  public:
    complex_property(type* obj) : obj(obj) {}
    complex_property(const complex_property&) = delete;
    complex_property& operator=(const int& x) {
      (obj->*setter)(x);
      return *this;
    }
    operator int(void) const { return (obj->*getter)(); }
  };

public:
  complex_property<&base::real, &base::real> x;
  complex_property<&base::imag, &base::imag> y;

  point(const int& x=int() , const int& y=int())
    : base(x, y), x(this), y(this) {}
  point(const point& p) : point(p.real(), p.imag()) {}
  point(const base& c) : point(c.real(), c.imag()) {}

  point& operator=(const base& c) {
    this->real(c.real());
    this->imag(c.imag());
    return *this;
  }
  point& operator=(const point& p) {
    this->real(p.real());
    this->imag(p.imag());
    return *this;
  }
};

#endif

以下のように使えます。

main.cpp
#include <iostream>
#include "point.hpp"

int main(void) {
  point p(1, 2);

  // メンバ x, y を通じてアクセスできる
  std::cout << "p.x=" << p.x << std::endl
            << "p.y=" << p.y << std::endl;

  // メンバに代入も可能
  p.x = 3;
  p.y = 4;
  std::cout << "p=" << p << std::endl;

  // complex から point への代入も可能
  std::complex<int> q(5, 6);
  p=q;
  std::cout << "p=" << p << std::endl;

  // point から complex への代入も可能
  q=point(7, 8);
  std::cout << "q=" << q << std::endl;

  // sin 関数にも入れられる
  std::cout << "sin(p)=" << std::sin(p) << std::endl;

  return 0;
}

もっとも、色々と扱いにくい部分が多いです。 あくまで余興なので詳細は説明しませんが、実用には難があります。

擬似的なメンバ関数

オブジェクトのメンバ関数を呼出すにはメンバアクセス演算子 (ピリオド) が用いられます。 そしてメンバアクセス演算子はオーバーロードすることができません。

なので、他の演算子をあたかもメンバアクセス演算子のように定義してメンバ関数 (に見えるもの) を追加するという手法があります。 ここでは演算子 ^ を擬似的なメンバアクセス演算子として定義してみます。

pseudo_member.hpp
#ifndef HEADER_0fcdb3cc27ec0c61a7686faebbfedb68
#define HEADER_0fcdb3cc27ec0c61a7686faebbfedb68

template<class T, class U, void (T::*)(U)> class setter;

template<class T, class U, void (T::*f)(U)>
void operator^(T& obj, const setter<T, U, f>& s) {
  (obj.*f)(s.arg);
}

template<class T, class U, void (T::*f)(U)>
class setter {
  friend void operator^<T, U, f>(T&, const setter&);
private:
  const U arg;
public:
  setter(const U arg) : arg(arg) {}
};

template<class T, class U, U (T::*)(void) const> class getter {};

template<class T, class U, U (T::*f)(void) const>
U operator^(T& obj, const getter<T, U, f>&) {
  return (obj.*f)();
}

#endif
extend_complex.hpp
#ifndef HEADER_b8de534721300a278226551d2ab44ced
#define HEADER_b8de534721300a278226551d2ab44ced

#include "pseudo_member.hpp"
#include <complex>

using point = std::complex<int>;

setter<point, int, &point::real> x(int n) {
  return setter<point, int, &point::real>(n);
}

getter<point, int, &point::real> x(void) {
  return getter<point, int, &point::real>();
}

setter<point, int, &point::imag> y(int n) {
  return setter<point, int, &point::imag>(n);
}

getter<point, int, &point::imag> y(void) {
  return getter<point, int, &point::imag>();
}

#endif

これで以下のように xy をメンバ関数もどきとして利用可能です。 . のかわりに ^ を使っているということを除けばあたかもメンバ関数を呼出しているいるように見えるのではないでしょうか。

main.cpp
#include "extend_complex.hpp"

#include <iostream>

int main(void) {
  point pnt(1,2);

  std::cout << pnt << std::endl;
  pnt^x(3);
  std::cout << (pnt^x()) << std::endl;
  std::cout << (pnt^y()) << std::endl;
  pnt^y(4);
  std::cout << pnt << std::endl;

  return 0;
}

^. に比べると結合の優先順位が低く、 . であれば不要であった括弧が必要になる場面が多いのは不満点ではあります。 かといってオーバーロード可能な優先順位の高い演算子となると四則演算などなので見た目にも奇妙になってしまいます。

まとめ

これらの手法の内、結局のところどれがよいでしょうか。 それぞれに利点・欠点があって一概にいえないものではありますが、私の感覚でいえば

非メンバ関数 >> 擬似的なメンバ関数 >> 継承 >> (越えられない壁) >> プロパティ >> マクロ

といったところだと思います。

7
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8