web-dev-qa-db-ja.com

テンプレート引数として渡された関数

引数としてC++テンプレート関数を渡すことに関するルールを探しています。

これは、次の例に示すように、C++でサポートされています。

#include <iostream>

void add1(int &v)
{
  v+=1;
}

void add2(int &v)
{
  v+=2;
}

template <void (*T)(int &)>
void doOperation()
{
  int temp=0;
  T(temp);
  std::cout << "Result is " << temp << std::endl;
}

int main()
{
  doOperation<add1>();
  doOperation<add2>();
}

ただし、この手法について学ぶことは困難です。 「テンプレート引数としての機能」のグーグル はあまり意味がありません。そして、古典的な C++ Templates The Complete Guide 驚くべきことにも(少なくとも私の検索からは)議論していません。

質問は、これが有効なC++(または単に広くサポートされている拡張機能)かどうかです。

また、この種のテンプレート呼び出し中に、同じシグネチャを持つファンクターを明示的な関数と交換可能に使用できるようにする方法はありますか?

以下は、少なくとも Visual C++ では、上記のプログラムではnot動作しません。構文が明らかに間違っているためです。カスタム比較操作を定義する場合に関数ポインターまたはファンクターをstd :: sortアルゴリズムに渡す方法と同様に、ファンクターの関数を切り替えることができれば、その逆も可能です。

   struct add3 {
      void operator() (int &v) {v+=3;}
   };
...

    doOperation<add3>();

1つまたは2つのWebリンク、またはC++テンプレートブックのページへのポインタをいただければ幸いです。

210
SPWorley

はい、有効です。

ファンクタでも動作するようにするために、通常の解決策はこのようなものです:

template <typename F>
void doOperation(F f)
{
  int temp=0;
  f(temp);
  std::cout << "Result is " << temp << std::endl;
}

現在、次のいずれかとして呼び出すことができます。

doOperation(add2);
doOperation(add3());

これに伴う問題は、コンパイラがadd2の呼び出しをインライン化するのが難しい場合、コンパイラが知っているのは関数ポインタ型void (*)(int &)doOperationに渡されることだけだからです。 (しかし、ファンクターであるadd3は簡単にインライン化できます。ここで、コンパイラーはadd3型のオブジェクトが関数に渡されることを知っています。つまり、呼び出す関数は未知の関数ポインターではなくadd3::operator()であるということです。)

118
jalf

テンプレートパラメータは、タイプ(typename T)または値(int X)でパラメータ化できます。

コードの一部をテンプレート化する「従来の」C++の方法は、ファンクターを使用することです。つまり、コードはオブジェクト内にあるため、オブジェクトはコードに一意の型を与えます。

従来の関数を使用する場合、型の変更はspecific関数を示すのではなく、多くの可能な関数のシグネチャのみを指定するため、この手法はうまく機能しません。そう:

template<typename OP>
int do_op(int a, int b, OP op)
{
  return op(a,b);
}
int add(int a, int b) { return a + b; }
...

int c = do_op(4,5,add);

ファンクターの場合と同等ではありません。この例では、do_opは、シグネチャがint X(int、int)であるすべての関数ポインターに対してインスタンス化されます。コンパイラは、このケースを完全にインライン化するためにかなり積極的でなければなりません。 (コンパイラの最適化がかなり進んでいるので、私はそれを除外しません。)

このコードは、私たちが望んでいることをまったく実行できないことを示す1つの方法です。

int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);

まだ合法であり、明らかにこれはインライン化されていません。完全なインライン化を実現するには、値でテンプレート化する必要があります。そのため、テンプレートで関数を完全に使用できます。

typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
 return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);

この場合、do_opのインスタンス化された各バージョンは、既に使用可能な特定の関数でインスタンス化されています。したがって、do_opのコードは「return a + b」のように見えると予想されます。 (LISPプログラマー、ニヤニヤしてはいけません!)

また、次の理由により、これが目的に近いことを確認できます。

int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);

コンパイルに失敗します。 GCCは次のように述べています。

2番目の例が本当に完全にopをインライン化していて、最初の例がそうでない場合、テンプレートは何が良いのでしょうか?何してるの?答えは次のとおりです。タイプ強制。最初の例のこのリフは機能します:

template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);

その例は動作します! (私はそれが良いC++であることを示唆していませんが...)起こったのは、do_opがさまざまな関数のsignaturesの周りにテンプレート化されていることです。したがって、faddを使用したdo_opのインスタンス化されたコードは次のようになります。

convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.

比較すると、値によるケースでは、関数の引数と完全に一致する必要があります。

62
Ben Supnik

関数ポインタはテンプレートパラメータとして渡すことができ、 これは標準C++の一部です です。ただし、テンプレートでは、関数へのポインターではなく関数として宣言および使用されます。テンプレートのインスタンス化では、名前だけでなく関数のアドレスを渡します。

例えば:

int i;


void add1(int& i) { i += 1; }

template<void op(int&)>
void do_op_fn_ptr_tpl(int& i) { op(i); }

i = 0;
do_op_fn_ptr_tpl<&add1>(i);

ファンクタータイプをテンプレート引数として渡したい場合:

struct add2_t {
  void operator()(int& i) { i += 2; }
};

template<typename op>
void do_op_fntr_tpl(int& i) {
  op o;
  o(i);
}

i = 0;
do_op_fntr_tpl<add2_t>(i);

いくつかの答えは、ファンクタインスタンスを引数として渡します。

template<typename op>
void do_op_fntr_arg(int& i, op o) { o(i); }

i = 0;
add2_t add2;

// This has the advantage of looking identical whether 
// you pass a functor or a free function:
do_op_fntr_arg(i, add1);
do_op_fntr_arg(i, add2);

テンプレート引数を使用してこの均一な外観に最も近い方法は、do_opを2回定義し、1回は非型パラメーターで、1回は型パラメーターで定義することです。

// non-type (function pointer) template parameter
template<void op(int&)>
void do_op(int& i) { op(i); }

// type (functor class) template parameter
template<typename op>
void do_op(int& i) {
  op o; 
  o(i); 
}

i = 0;
do_op<&add1>(i); // still need address-of operator in the function pointer case.
do_op<add2_t>(i);

正直なところ、私は実際にこれがコンパイルされないことを期待していましたが、gcc-4.8とVisual Studio 2013でうまくいきました。

15
Kietz

テンプレートで

template <void (*T)(int &)>
void doOperation()

パラメーターTは、非型テンプレートパラメーターです。これは、テンプレート関数の動作がパラメーターの値によって変化することを意味します(コンパイル時に修正する必要があり、関数ポインター定数はこれです)。

関数オブジェクトと関数パラメーターの両方で動作するものが必要な場合は、型付きテンプレートが必要です。ただし、これを行う場合は、実行時に関数にオブジェクトインスタンス(関数オブジェクトインスタンスまたは関数ポインター)を提供する必要もあります。

template <class T>
void doOperation(T t)
{
  int temp=0;
  t(temp);
  std::cout << "Result is " << temp << std::endl;
}

いくつかのマイナーなパフォーマンスの考慮事項があります。この新しいバージョンは、特定の関数ポインターは参照のみされて実行時に呼び出されるため、関数ポインター引数では効率が低下する可能性がありますが、関数ポインターテンプレートは使用される特定の関数ポインターに基づいて最適化できます(インラインで関数呼び出しが可能)。特定のoperator()は関数オブジェクトの型によって完全に決定されるため、関数オブジェクトは型付きテンプレートを使用して非常に効率的に展開できます。

8
CB Bailey

ファンクターの例が機能しないのは、operator()を呼び出すためのインスタンスが必要だからです。

1

編集:演算子を参照として渡すと機能しません。簡単にするために、関数ポインターとして理解してください。参照ではなく、ポインタを送信するだけです。あなたはこのようなものを書こうとしていると思います。

struct Square
{
    double operator()(double number) { return number * number; }
};

template <class Function>
double integrate(Function f, double a, double b, unsigned int intervals)
{
    double delta = (b - a) / intervals, sum = 0.0;

    while(a < b)
    {
        sum += f(a) * delta;
        a += delta;
    }

    return sum;
}

。 。

std::cout << "interval : " << i << tab << tab << "intgeration = "
 << integrate(Square(), 0.0, 1.0, 10) << std::endl;
0
AraK