【问题标题】:Function passed as template argument作为模板参数传递的函数
【发布时间】:2010-11-13 12:51:10
【问题描述】:

我正在寻找涉及将 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>();
}

但是,学习这种技术很困难。 Googling for "function as a template argument" 不会导致太多。而经典的C++ Templates The Complete Guide 令人惊讶地也没有讨论它(至少不是来自我的搜索)。

我的问题是这是否是有效的 C++(或只是一些广泛支持的扩展)。

另外,在这种模板调用期间,有没有办法允许具有相同签名的函子与显式函数互换使用?

在上述程序中不起作用,至少在Visual C++ 中,因为语法显然是错误的。能够为仿函数切换函数会很好,反之亦然,类似于如果您想定义自定义比较操作,您可以将函数指针或仿函数传递给 std::sort 算法。

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

    doOperation<add3>();

如果指向一个或两个 Web 链接,或 C++ 模板书中的一页,我们将不胜感激!

【问题讨论】:

标签: c++ templates code-generation functor


【解决方案1】:

是的,它是有效的。

至于让它与仿函数一起使用,通常的解决方案是这样的:

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

现在可以称为:

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

See it live

这样做的问题是,如果编译器难以内联对add2 的调用,因为编译器只知道函数指针类型void (*)(int &amp;) 正在传递给doOperation。 (但是add3,作为一个函子,可以很容易内联。这里,编译器知道一个add3类型的对象被传递给函数,这意味着要调用的函数是add3::operator(),而不仅仅是一些未知函数指针。)

【讨论】:

  • 现在有一个有趣的问题。当传递一个函数名时,它不像是一个函数指针。这是一个显式函数,在编译时给出。所以编译器在编译时就知道它得到了什么。
  • 使用仿函数优于函数指针。仿函数可以在类内部实例化,从而为编译器提供更多的优化操作(例如内联)。编译器很难优化对函数指针的调用。
  • 当函数在模板参数中使用时,它“衰减”为指向传递函数的指针。这类似于数组在作为参数传递给参数时如何衰减为指针。当然,指针值在编译时是已知的,并且必须指向具有外部链接的函数,以便编译器可以使用此信息进行优化。
  • 几年后,使用函数作为模板参数的情况在 C++11 中得到了很大改善。您不再需要像仿函数类那样使用 Javaism,并且可以直接使用静态内联函数作为模板参数。与 1970 年代的 Lisp 宏相比仍然相去甚远,但 C++11 这些年来确实取得了不错的进步。
  • 既然 c++11 将函数作为右值引用 (template &lt;typename F&gt; void doOperation(F&amp;&amp; f) {/**/}) 不是更好,所以例如 bind 可以传递一个 bind-expression 而不是绑定它?
【解决方案2】:

模板参数可以按类型(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。编译器必须非常激进才能完全内联这种情况。 (不过我不排除它,因为编译器优化已经相当先进了。)

判断这段代码没有达到我们想要的效果的一种方法是:

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 说:“错误:'func_ptr' 不能出现在常量表达式中。换句话说,我无法完全展开 do_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.

相比之下,我们的按值案例要求函数参数完全匹配。

【讨论】:

【解决方案3】:

函数指针可以作为模板参数传递,this is part of standard 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 两次——一次使用非类型参数,一次使用类型参数。

// 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。

【讨论】:

    【解决方案4】:

    在您的模板中

    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() 完全由函数对象的类型决定。

    【讨论】:

      【解决方案5】:

      您的仿函数示例不起作用的原因是您需要一个实例来调用operator()

      【讨论】:

        【解决方案6】:

        带着额外的要求来到这里,参数/返回类型也应该有所不同。 在 Ben Supnik 之后,这将适用于某些类型的 T

        typedef T(*binary_T_op)(T, T);
        

        而不是

        typedef int(*binary_int_op)(int, int);
        

        这里的解决方案是将函数类型定义和函数模板放入一个环绕的struct模板中。

        template <typename T> struct BinOp
        {
            typedef T(*binary_T_op )(T, T); // signature for all valid template params
            template<binary_T_op op>
            T do_op(T a, T b)
            {
               return op(a,b);
            }
        };
        
        
        double mulDouble(double a, double b)
        {
            return a * b;
        }
        
        
        BinOp<double> doubleBinOp;
        
        double res = doubleBinOp.do_op<&mulDouble>(4, 5);
        

        BinOp 也可以是一个带有静态方法模板 do_op(...) 的类,然后调用为

        double res = BinOp<double>::do_op<&mulDouble>(4, 5);
        

        编辑

        受 0x2207 评论的启发,这里有一个仿函数,它可以接受任何具有两个参数和可转换值的函数。

        struct BinOp
        {
            template <typename R, typename S, typename T, typename U, typename V> R operator()(R (*binaryOp )(S, T), U u, V v)
            {
                return binaryOp(u,v);
            }
        
        };
        
        double subD(double a, int b)
        {
            return a-b;
        }
        
        int subI(double a, int b)
        {
            return (int)(a-b);
        }
        
        
        int main()
        {
            double resD = BinOp()(&subD, 4.03, 3);
            int resI = BinOp()(&subI, 4.03, 3);
        
            std::cout << resD << std::endl;
            std::cout << resI << std::endl;
            return 0;
        }
        

        正确计算为 double 1.03 和 int 1

        【讨论】:

        • 是否有可能从&amp;mulDuouble 类型派生T?因为现在你必须指定double 两次。
        【解决方案7】:

        编辑:将运算符作为参考传递是行不通的。为简单起见,将其理解为函数指针。您只需发送指针,而不是参考。 我认为您正在尝试编写类似的内容。

        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;
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2023-03-26
          • 1970-01-01
          • 1970-01-01
          • 2017-06-14
          • 1970-01-01
          • 1970-01-01
          • 2019-11-16
          • 1970-01-01
          相关资源
          最近更新 更多