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


224

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 ++テンプレートの完全ガイドでは、驚くべきことに、それについても説明していません(少なくとも私の検索では)。

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

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

上記のプログラムでは、少なくともVisual C ++では、構文が明らかに間違っているため、以下は機能しませ。カスタム比較演算を定義したい場合に関数ポインタまたはファンクタをstd :: sortアルゴリズムに渡すことができるのと同じように、ファンクタの関数を切り替えたり、その逆を行うことができると便利です。

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

    doOperation<add3>();

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


テンプレート引数としての関数の利点は何ですか?戻り値の型はテンプレート型として使用されませんか?
DaClown 2009

関連:キャプチャのないラムダは関数ポインタに減衰する可能性があり、C ++ 17ではそれをテンプレートパラメータとして渡すことができます。Clangは問題なくコンパイルできますが、現在のgcc(8.2)にはバグがあり、を使用しても「リンケージがない」と誤って拒否され-std=gnu++17ます。 C ++ 17キャプチャレスラムダconstexpr変換演算子の結果を、関数ポインターテンプレートの非型引数として使用できますか?
Peter Cordes

回答:


123

はい、有効です。

ファンクタでも機能するようにするには、通常の解決策は代わりに次のようなものです:

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()であり、不明な関数ポインターだけではありません。)


19
ここで興味深い質問です。関数名が渡されると、関数ポインタが含まれるようにはなりません。これは、コンパイル時に指定される明示的な関数です。そのため、コンパイラーはコンパイル時に取得したものを正確に認識します。
SPWorley 2009

1
関数ポインタよりもファンクタを使用する方が有利です。ファンクタはクラス内でインスタンス化できるため、コンパイラーに最適化(インライン化など)の操作性を提供します。コンパイラーは、関数ポインターを介した呼び出しを最適化することが難しいでしょう。
マーティンヨーク

11
関数がテンプレートパラメータで使用されると、渡された関数へのポインタに「減衰」します。配列が引数としてパラメーターに渡されたときに、配列がどのようにポインターに減衰するかに類似しています。もちろん、ポインター値はコンパイル時に認識され、コンパイラーが最適化の目的でこの情報を使用できるように、外部リンケージのある関数を指す必要があります。
CBベイリー

5
数年後、C ++ 11ではテンプレート引数として関数を使用する状況が大幅に改善されました。ファンクタクラスのようなJavaismを使用する必要がなくなり、たとえば静的インライン関数をテンプレート引数として直接使用できます。1970年代のLispマクロに比べるとまだまだはるかに優れていますが、C ++ 11は明らかに長年にわたって進歩しています。
pfalcon 2013

5
c ++ 11は関数を右辺値参照(template <typename F> void doOperation(F&& f) {/**/})として使用する方が良いのではないでしょうか。たとえば、bindはバインドする代わりにbind-expressionを渡すことができますか?
user1810087

70

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

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

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

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);

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

このコードが私たちが望んでいることを完全に実行していないことを伝える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番目の例が実際に完全にインライン化されており、最初の例がそうでない場合、テンプレートはどのように役立ちますか?何してるの?答えは、型強制です。最初の例のこのリフは機能します。

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がさまざまな関数のシグネチャの周りにテンプレート化されており、それぞれの個別のインスタンス化が異なる型強制コードを書き込みます。したがって、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.

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


2
「明らかにインライン化されていない」というここでの観察に対する直接の回答としてのフォローアップの質問については、stackoverflow.com / questions / 13674935 /…を参照してくださいint c = do_op(4,5,func_ptr);
Dan Nissenbaum

これがインライン化されている例については、こちらをご覧ください:stackoverflow.com/questions/4860762/…コンパイラは最近かなりスマートになっているようです。
BigSandwich 2013

15

関数ポインタはテンプレートパラメータとして渡すことができ、これは標準の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を使用するとうまくいきました。


9

テンプレート内

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()、関数オブジェクトの型によって完全に特定されます。



0

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

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;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.