highlight

2017年1月27日金曜日

c++ 抽象クラスを関数テンプレート実引数に渡したい

c++ では、抽象クラスを関数テンプレートの実引数に使うことが出来ない事になっている。 これを何とかしてブチ込む必要がある場合には、どうすれば良いのか。

具体例を挙げる。以下のような AbstractName という純粋仮想クラスを、 関数テンプレートとして定義されている tfrun に渡したいような、func0() 実装中の様な場面。


#include <iostream>
// 抽象クラス
struct AbstractName {
  virtual operator const char*() const = 0;
};

struct DerivedName : public AbstractName {
  virtual operator const char*() const override {
    return "Derived";
  }
};

template <typename Type>
void tfrun(Type obj)
{
  std::cout << "tfrun" << obj << std::endl;
}

void func0(const AbstractName& name)
{
  /* error!
   * g++    : invalid abstract parameter type
   * clang++: parameter type is an abstract class
   */
  tfrun(name);
}

int main(int, const char**)
{
  DerivedName n;

  func0(n);

  return 0;
}

上のコードだと、関数テンプレート tfrun<const AbstractName&>(name) に展開する箇所でエラーとなる。 因みに、これが参照ではなくポインタであるか、渡す先が関数テンプレートではなくて単に const AbstractName& を受け取る関数ならば、当然ながら問題なくコンパイルできる。 このような処理を動かすにはどうすれば良いのか、いくつか方法を考える。

入力側からポインタを受け取るように変更する

ほな、ポインタにすればええやんか、ということで、上のコードでいうと、func0() の引数を const AbstractName* に変えられるのなら変えた上で、関数テンプレートの部分特殊化を定義すれば良い。 だが、この関数の実装には手出しが出来ない様な状況では採用できない。


template <typename Type>
void tfrun(const Type* obj)
{
  std::cout << "tfrun<AbstractName*>:"
            << *obj
            << std::endl;
}

void func1(const AbstractName* name)
{
  tfrun(name);
}

int main(int, const char**)
{
  DerivedName n;

  // func0(n);
  func1(&n);

  return 0;
}

ポインタを受け取る関数テンプレートを使う

入力側の参照をポインタに変換するという方法。ポインタ型用の部分特殊化は上のものを流用する。


void func0(const AbstractName& name)
{
  tfrun(&name);
}

シンプルにアドレス演算子を使う方法。今回のケースではこれがベストアンサーだと思う。 ここで、std::addressof() の事を思い出す。そういえば、もしも AbstractNameoperator& を実装していたら、こちらを使わざるを得ない。実際 operator& をオーバーライドしているクラスにはお目にかかったことが無いが。


void func0(const AbstractName& name)
{
  // cast
  tfrun(reinterpret_cast<const AbstractName*>(
          &const_cast<char&>(
            reinterpret_cast<const volatile char&>(name))));
}

この黒魔術の様なキャスト。addressof() の実装だが、一つずつ見ていくと、一旦 const volatile char& にキャストし、const_castvolatile, const を外す。この時点で対象は char 型の参照と見做され operator&() の呼び出しを回避しつつ、& 演算子でアドレスを取得、その後目的の型にキャストする。std::addressof<T>() のお陰で実際には上のような書く必要はない。memory ヘッダを include しよう。


void func0(const AbstractName& name)
{
  // std::addressof
  tfrun(std::addressof(name));
}

関数テンプレートをクラステンプレートにする

関数をそのまま関数オブジェクトのテンプレートとして移植する。コードの量はどうしても増える。


template <typename Type>
struct Runner1 {
  void run(Type obj) {
    std::cout << obj << std::endl;
  }
};

void func0(const AbstractName& name)
{
  Runner1<const AbstractName&> runner;
  runner.run(name);
}

std::reference_wrapper を使う

参照をラップして、ポインタにして保持するクラスが標準ライブラリに用意されているので、それを利用する。これは functional ヘッダに入っているはず。


using RefWrapType = std::reference_wrapper<const AbstractName>;

template <>
void tfrun(RefWrapType obj)
{
  std::cout << "wtfrun<RefWrapType>:"
          // error operator T&()
          // g++  : invalid cast to abstract clas type
          // clang: allocating an object of abstract class type
          // << static_cast<RefWrapType::type>(obj).get()
            << obj.get()
            << ", sizeof(RefWrapType):" << sizeof(RefWrapType)
            << std::endl;
}

void func0(const AbstractName& name)
{
  tfrun(RefWrapType{name});
}

簡単にまとめる。

  • 入力側をポインタに変更できれば、ポインタにしてやるのが簡単。だが、呼び出し側のコードに、確実に全てアドレス演算子を付ける変更が必要となる。
  • ポインタを渡す方法。ポインタ型のテンプレートを用意するだけで使えるし、最もシンプルな解法じゃないかなと思う。テンプレートメタプログラミングの真っ只中なら、必ず std::addressof() を使おう。
  • クラステンプレートにする方法は、コードも増えるし、既に別の型で関数テンプレートを使ってたら、そちらも変更する必要が出てくるかもしれない。最も面倒くさい手段だと思われる。
  • std::reference_wrapper を使うのは、ポインタのサイズの分だけ、僅かにメモリを使うことになるが、メンバ関数やメンバ型の type とかを使いたい様な場合には便利な場合があるかもしれない。

結論: アドレス演算子 & を使うのが良い。operator& を持つ型が渡される可能性があれば、std::addressof() を使うのが良い。

0 件のコメント:

コメントを投稿

スパムフィルタが機能しないようなので、コメント不可にしました。

注: コメントを投稿できるのは、このブログのメンバーだけです。