c++ templateを用いてオブジェクト指向でクラスを合成拡張する


オブジェクトの拡張

みなさん、オブジェクトを拡張していますか。
あんな機能が足りない、この情報と組み合わせれば提供できるのに・・・。
そんなときにはフリー関数ではなく拡張することになります。
今回は拡張のうち、流行りの移譲による合成を上手く行うためにtemplateを活用してゆこうという内容です。

前提

コードはc++14を最低にします。
完全にシグネチャを同一にするためにc++14のdecltype(auto)を用います。

概要

decltype(auto) と 可変引数テンプレート、完全転送を組み合わせてオーバーロード呼び出しを全て移譲します。
オーバーロードまでなので、関数の名前ごとに同じような移譲処理を書かなければなりません。

移譲用の関数と同じ名前の関数を拡張したいときは工夫が必要です。

オブジェクトの合成とは

オブジェクトの合成とは、拡張したいオブジェクトを所有し、処理を移譲することで元の機能を提供する手法である。

移譲しないことで機能を制限したり、移譲せずに独自の機能を提供できる。このため、オブジェクトの機能の拡張に用いられる。
拡張の他の方法である継承と違って、誤って親オブジェクトに代入されたり、リファレンスを取られる事故が起きない。更に元のオブジェクトが変更されてもその変更に破壊的影響を受けにくい利点がある。

しかし最も重要なのは、単純に拡張するつもりでも移譲するプログラムを書かなければならないということです。

全てを移譲するには

オーバーロードを持つBaseを書くと長くなるので、オーバーロードが多い使ってくれる人も少ないstd::valarray<int>を拡張します。

#include <initializer_list>
#include <utility>
#include <valarray>

class MyVector
{
public:
  template<typename... Args>
  MyVector(Args&&... args)
    : array_(std::forward<Args>(args)...)
  {
  }

  MyVector(std::initializer_list<int> init)
    : array_(init)
  {
  }

  template<typename... Args> // (1)
  decltype(auto) operator[](Args&&... args) // (2)
  {
    return array_.operator[](std::forward<Args>(args)...); // (3)
  }

private:
  std::valarray<int> array_;
};

上の例では11種のコンストラクタの全移譲と11種のoperator[]の全移譲を行っています。
これだけだと拡張ではなく制限ですが、適当な関数を加えれば拡張になります。

コードについて

(1)では可変引数テンプレートによってあらゆる引数を受け入れています。
(2)ではdecltype(auto)を用いることで、valarray::operator[]の帰り値で特に厄介な参照返しと値返しをそれぞれ正しく返却しています。
ここで誤ってautoを用いると、参照が正しく返却されません。
また、テンプレートパラメータパックをUniversal referenceで受け取っています。
(3)では移譲先の関数にforwardを使うことで引数を完全転送しています。返り値はdecltype(auto)が正しく推論してくれます。

これでstd::valarrayの11種のコンストラクタ全てと11種のoperator[]全てを移譲できました。
以下のようなコードが実行できます。

#include <iostream>

// Include MyVector definition

int main()
{
  {
    MyVector vec(10);
    vec[5] = 3;
    std::cout << "vec[5]: " << vec[5] << '\n'; // 3
  }
  {
    MyVector vec {9, 1, 8, 2, 7, 3, 6, 4, 5};
    std::cout << "vec[2]: " << vec[2] << '\n'; // 8
  }
}

移譲先のセマンティクスのみ有効にするには

全てを転送していると、移譲先が受け取れない引数を転送して、template特有のエラーメッセージが出たり、オーバーロードを追加する際にテンプレートの一致度に悩まされたりします。

SFINAE として処理されるところへ移譲する式を入れることで移譲できない引数を除外できます。
若干型システムレベルで内部構造が透けますが、そもそも移譲先のクラスへ転送する関数なので特記するような問題は無いはずです。
欠点としては、同じような記述を二回することぐらいです。

#include <utility>

// MyVector member function
template<typename... Args>
auto operator[](Args&&... args) -> decltype(std::declval<std::valarray<int>>().operator[](std::forward<Args>(args)...))
{
  return array_.operator[](std::forward<Args>(args)...);
}

同じ名前の関数を拡張する

今まではどのように移譲するかという話でしたが、拡張用関数を追加する際に、移譲用の関数と名前が被ると次のような対策が必要になります。

移譲先に存在しない引数を拡張する場合

上記の 移譲先のセマンティクスのみ有効にするには に従うことで移譲先に定義されていない引数を自由に追加することができます。一番自然な形になります。

移譲先のセマンティクスのみ有効にするにはをしたくないときや、移譲先に存在する引数を拡張するときは次以降の解決策を試みます。

オーバーロードを完全に定義する

可変引数テンプレートとUniversal referenceを組み合わせた状態からほぼ正しく拡張するには引数の3乗の数のオーバーロードを定義する必要があります(constのみ)。完全に正しく拡張するには6乗必要になります(cv修飾)。
引数が2つでも9個のオーバーロードになります。1つなら3個なのでぎりぎり許容できるかもしれません。必要に応じてstructを作り、引数をひとつにして以下のようにします。

template<typename... Args>
decltype(auto) f(Args&&... args){}

int f(const int&) {}
int f(int&) {}
int f(int&&) {}
// int f(volatile const int&) {}
// int f(volatile int&) {}
// int f(volatile int&&) {}

上のようにconst lvalue, lvalue, rvalueをそれぞれ受け取る関数をオーバーロードすることで、全ての参照をそれぞれの関数で実行できます。

特に注意が必要なのはconst int&だけでなくint&も必要なことです。
テンプレートはきちんと正確な型を推定するので、int&版の関数を生成してきます。するとconst int&より一致度が高くなって、テンプレート版の関数が使われてしまいます。
きちんと全て定義しましょう。中で別の関数を呼び出すのは問題ないです。

なおvolatile int&&はvolatile変数をstd::moveすれば作れます。
標準ライブラリはmove後再代入できるので実は効率的だったりする・・・のかもしれない。

数の違いで識別する

オーバーロードするのに6個も同じような関数を書いてはいられないと言う人へ、
テンプレートの力を限定して区別がつくようにしましょう。

template<typename T>
decltype(auto) f(T&&) {}

template<typename T1, typename T2>
decltype(auto) f(T1&&, T2&&) {}

int f(const int&, const int&, const int&) {}

この例では引数が1個と2個のときはテンプレートで、引数が3つの時は普通の関数が使われます。
const lvalue referenceは様々なlvalueをバインドできるので、ひとつ書くだけでうまくいきます。
しかしながら、当然引数が3つの場合は移譲できません。

まとめ

移譲するには以下のパターンを使う。

#include <utility>

class C
{
  using super_type = /* Define having value type */
  super_type value_;

public:
  template<typename... Args>
  auto func(Args... args) -> decltype(std::declval<super_type>().func(std::forward<Args>(args)...))
  {
    value_.func(std::forward<Args>(args)...);
  }
};

移譲用と同じ名前でかつ、移譲先の引数と同じ型をオーバーロードしたいときは以下のどちらかを使う。

#include <utility>
using return_type = // Define extend type

// --------------------
// (1) Perfect overload
struct Arguments {/* Define argument values */};

return_type func(const Arguments&) {}
return_type func(Arguments&) {}
return_type func(Arguments&&) {}
return_type func(volatile const Arguments&) {}
return_type func(volatile Arguments&) {}
return_type func(volatile Arguments&&) {}
// --------------------
// (2) Select template
template<typename T>
auto func(T&& arg) -> decltype(std::declval<supper_type>().func(std::forward<Args>(arg)))
{
  value_.func(std::forward<T>(arg));
}

using value1_type = // Argument 1
using value2_type = // Argument 2
return_type func(const value1_type&, const value2_type&) {}