【问题标题】:C++: Function wrapper that behaves just like the function itselfC++:行为就像函数本身一样的函数包装器
【发布时间】:2010-10-27 03:16:29
【问题描述】:

如何编写一个可以包装任何函数并且可以像函数本身一样被调用的包装器?

我需要这个的原因:我想要一个 Timer 对象,它可以包装一个函数并像函数本身一样运行,而且它记录所有调用的累积时间。

场景如下所示:

// a function whose runtime should be logged
double foo(int x) {
  // do something that takes some time ...
}

Timer timed_foo(&foo); // timed_foo is a wrapping fct obj
double a = timed_foo(3);
double b = timed_foo(2);
double c = timed_foo(5);
std::cout << "Elapsed: " << timed_foo.GetElapsedTime();

我该如何编写这个Timer 类?

我正在尝试这样的事情:

#include <tr1/functional>
using std::tr1::function;

template<class Function>
class Timer {

public:

  Timer(Function& fct)
  : fct_(fct) {}

  ??? operator()(???){
    // call the fct_,   
    // measure runtime and add to elapsed_time_
  }

  long GetElapsedTime() { return elapsed_time_; }

private:
  Function& fct_;
  long elapsed_time_;
};

int main(int argc, char** argv){
    typedef function<double(int)> MyFct;
    MyFct fct = &foo;
    Timer<MyFct> timed_foo(fct);
    double a = timed_foo(3);
    double b = timed_foo(2);
    double c = timed_foo(5);
    std::cout << "Elapsed: " << timed_foo.GetElapsedTime();
}

(顺便说一句,我知道gprof 和其他用于分析运行时的工具,但是拥有这样一个Timer 对象来记录一些选定函数的运行时对我来说更方便。)

【问题讨论】:

  • 必须是 C++ 吗?如果你不介意“弄脏你的手”,你可以使用 C 的 varargs 破解一些东西......

标签: c++ function wrapper functional-programming tr1


【解决方案1】:

基本上,您想要做的事情在当前的 C++ 中是不可能的。对于要包装的任意数量的函数,您需要重载

const reference
non-const reference

但是它仍然不是完美的转发(一些边缘情况仍然存在),但它应该可以正常工作。如果您将自己限制为 const 引用,则可以使用这个(未测试):

template<class Function>
class Timer {
    typedef typename boost::function_types
       ::result_type<Function>::type return_type;

public:

  Timer(Function fct)
  : fct_(fct) {}

// macro generating one overload
#define FN(Z, N, D) \
  BOOST_PP_EXPR_IF(N, template<BOOST_PP_ENUM_PARAMS(N, typename T)>) \
  return_type operator()(BOOST_PP_ENUM_BINARY_PARAMS(N, T, const& t)) { \
      /* some stuff here */ \
      fct_(ENUM_PARAMS(N, t)); \
  }

// generate overloads for up to 10 parameters
BOOST_PP_REPEAT(10, FN, ~)
#undef FN

  long GetElapsedTime() { return elapsed_time_; }

private:
  // void() -> void(*)()
  typename boost::decay<Function>::type fct_;
  long elapsed_time_;
};

请注意,对于返回类型,您可以使用 boost 的函数类型库。那么

Timer<void(int)> t(&foo);
t(10);

你也可以使用纯值参数重载,然后如果你想通过引用传递一些东西,使用boost::ref。这实际上是一种非常常见的技术,尤其是在要保存此类参数时(此技术也用于boost::bind):

// if you want to have reference parameters:
void bar(int &i) { i = 10; }

Timer<void(int&)> f(&bar);
int a; 
f(boost::ref(a)); 
assert(a == 10);

或者您可以按照上面的说明为 const 和非 const 版本添加这些重载。查看Boost.Preprocessor 了解如何编写正确的宏。

您应该意识到,如果您希望能够传递任意可调用对象(不仅仅是函数),整个事情将会变得更加困难,因为您需要一种方法来获取它们的结果类型(这并不那么容易) . C++1x 将使这种事情变得更容易。

【讨论】:

  • 太棒了!我只是试图记住 enable_if 的东西,并打算在没有宏的情况下实现它(我不知道)。你节省了我的时间。谢谢。
  • 正如您所暗示的,使用 C++0x(我猜是 1x)完美转发变得非常容易。
  • 我不知道如何礼貌地问这个问题,但你能用 C++1{1,4,7} 中提供的“更简单”的方法更新这个吗?跨度>
【解决方案2】:

这是一种简单包装函数的方法。

template<typename T>
class Functor {
  T f;
public:
  Functor(T t){
      f = t;
  }
  T& operator()(){
    return f;
  }
};


int add(int a, int b)
{
  return a+b;
}

void testing()
{
  Functor<int (*)(int, int)> f(add);
  cout << f()(2,3);
}

【讨论】:

  • 总体上是个好主意。但它不允许测量时间,因为函数调用完成后没有代码可以运行。
  • 是的,您只能在 operator() 中运行代码。不幸的是,C++ 并不是为支持函数式风格而设计的。为了能够做到magic,你必须绕过这些限制。但是,嘿,这就是我们喜欢编程的原因之一 :)
【解决方案3】:

我假设您需要它来进行测试,并且不会将它们用作真正的代理或装饰器。所以你不需要使用 operator() 并且可以使用任何其他更不方便的调用方法。

template <typename TFunction>
class TimerWrapper
{
public:
    TimerWrapper(TFunction function, clock_t& elapsedTime):
        call(function),
        startTime_(::clock()),
        elapsedTime_(elapsedTime)
    {
    }

    ~TimerWrapper()
    {
        const clock_t endTime_ = ::clock();
        const clock_t diff = (endTime_ - startTime_);
        elapsedTime_ += diff;
    }

    TFunction call;
private:
    const clock_t startTime_;
    clock_t& elapsedTime_;
};


template <typename TFunction>
TimerWrapper<TFunction> test_time(TFunction function, clock_t& elapsedTime)
{
    return TimerWrapper<TFunction>(function, elapsedTime);
}

所以要测试你的一些函数,你应该只使用 test_time 函数而不是直接的 TimerWrapper 结构

int test1()
{
    std::cout << "test1\n";
    return 0;
}

void test2(int parameter)
{
    std::cout << "test2 with parameter " << parameter << "\n";
}

int main()
{
    clock_t elapsedTime = 0;
    test_time(test1, elapsedTime).call();
    test_time(test2, elapsedTime).call(20);
    double result = test_time(sqrt, elapsedTime).call(9.0);

    std::cout << "result = " << result << std::endl;
    std::cout << elapsedTime << std::endl;

    return 0;
}

【讨论】:

  • +1,因为这是恕我直言的好方法。只是需要注意的一点:计时器对象在完整表达式的末尾被销毁。所以如果你这样做 f(test_time(test1, elapsedTime).call(20));时间也将包括“f”之一。只是让你知道。由于它仅用于测试目的,因此可能无关紧要,因为可以避免它。
  • 如果你愿意,你也可以使用代理函数。然后它看起来像这样: test_time(test2, elapsedTime)(20);为此, TimerWrapper 需要这个: operator TFunction() { return call; } 现在,如果你写“(20)”,编译器会将定时器转换为函数指针并用参数调用它。没有像我的回答那样讨厌的 op() 重载:)
  • +1。我喜欢你如何通过直接公开函数对象成员来解决整个转发问题!我从来没有想过。
  • @litb:我从未考虑过转换函数可以转换为指向函数的类型,或者编译器在看到函数调用语法时会尝试这些类型转换——但它确实有效!看起来这种方法实际上在某种程度上解决了 C++ 中的转发问题:您可以将任何“预调用”代码放在转换函数中运行,我们只是缺少放置“后调用”代码的地方.但在很多情况下,使用临时对象的析构函数就足够了。有什么想法吗?
【解决方案4】:

如果您查看包含的 std::tr1::function 的实现,您可能会找到答案。

在 c++11 中,std:: 函数是用可变参数模板实现的。使用这样的模板,您的计时器类可能看起来像

template<typename>
class Timer;

template<typename R, typename... T>
class Timer<R(T...)>
{
    typedef R (*function_type)(T...);

    function_type function;
public:
    Timer(function_type f)
    {
        function = f;
    }

    R operator() (T&&... a)
    {
        // timer starts here
        R r = function(std::forward<T>(a)...);
        // timer ends here
        return r;
    }
};

float some_function(int x, double y)
{
    return static_cast<float>( static_cast<double>(x) * y );
}


Timer<float(int,double)> timed_function(some_function); // create a timed function

float r = timed_function(3,6.0); // call the timed function

【讨论】:

    【解决方案5】:

    Stroustrup 展示了一种函数包装(注入)技能,可重载operator-&gt;。关键思想是:operator-&gt;会重复调用,直到遇到原生指针类型,所以让Timer::operator-&gt;返回一个临时对象,临时对象返回它的指针。然后会发生以下情况:

    1. 创建了临时对象(调用了ctor)。
    2. 已调用目标函数。
    3. temp obj 已破坏(调用 dtor)。

    您可以在 ctor 和 dtor 中注入任何代码。像这样。

    template < class F >
    class Holder {
    public:
        Holder  (F v) : f(v) { std::cout << "Start!" << std::endl ; }
        ~Holder ()           { std::cout << "Stop!"  << std::endl ; }
        Holder* operator->() { return this ; }
        F f ;
    } ;
    
    template < class F >
    class Timer {
    public:
        Timer ( F v ) : f(v) {}
        Holder<F> operator->() { Holder<F> h(f) ; return h ; }
        F f ;
    } ;
    
    int foo ( int a, int b ) { std::cout << "foo()" << std::endl ; }
    
    int main ()
    {
        Timer<int(*)(int,int)> timer(foo) ;
        timer->f(1,2) ;
    }
    

    实现和使用都很简单。

    【讨论】:

    • +1,是的,这是一个好方法。但正如 litb 在 Mykola Golubyev 的回答中指出的那样,临时将在包含完整表达式的末尾被删除,这可能与调用 f 后立即不同——例如"big_slow_function(timer->f(1, 2))" 实际上也包括运行 big_slow_function() 的时间。这不是一个严重的错误,只是需要注意的事情。
    • 供参考,Stroustrup 的原描述can be found here
    【解决方案6】:

    使用宏和模板的解决方案:例如你想换行

    double foo( double i ) { printf("foo %f\n",i); return i; }
    double r = WRAP( foo( 10.1 ) );
    

    在调用 foo() 之前和之后,应该调用包装函数 beginWrap() 和 endWrap()。 (endWrap() 是一个模板函数。)

    void beginWrap() { printf("beginWrap()\n"); }
    template <class T> T endWrap(const T& t) { printf("endWrap()\n"); return t; }
    

    #define WRAP(f) endWrap( (beginWrap(), f) );
    

    使用逗号操作符的优先级来确保 beginWrap() 被首先调用。 f 的结果被传递给 endWrap() ,它只是返回它。 所以输出是:

    beginWrap()
    foo 10.100000
    endWrap()
    

    结果 r 包含 10.1。

    【讨论】:

      【解决方案7】:

      如果您希望创建一个可以包装和调用任意函数的泛型类,那么您将面临巨大的挑战。在这种情况下,您必须使仿函数(operator())返回 double 并将 int 作为参数。然后,您创建了一个类族,它们可以调用具有相同签名的所有函数。只要您想添加更多类型的函数,就需要该签名的更多函子,例如

      MyClass goo(double a, double b)
      {
         // ..
      }
      
      template<class Function>
      class Timer {
      
      public:
      
        Timer(Function& fct)
        : fct_(fct) {}
      
        MyClass operator()(double a, double b){
      
        }
      
      };
      

      编辑:一些拼写错误

      【讨论】:

      • 请注意,仅当您对包装函数的实际结果感兴趣时才需要运算符的返回值,即上面的运算符也可以是一个 void。
      【解决方案8】:

      我不太清楚你在看什么。但是,对于给定的示例,它很简单:

      void operator() (int x)
      {
         clock_t start_time = ::clock();    // time before calling
         fct_(x);                           // call function
         clock_t end_time = ::clock();      // time when done
      
         elapsed_time_ += (end_time - start_time) / CLOCKS_PER_SEC;
      }
      

      注意:这将以秒为单位测量时间。如果您想拥有高精度计时器,您可能必须检查操作系统的特定功能(例如 Windows 上的 GetTickCountQueryPerformanceCounter)。

      如果你想要一个通用的函数包装器,你应该看看Boost.Bind,这会很有帮助。

      【讨论】:

      • 我不是在问如何测量运行时间。我想要一个可以包装任何函数并提供与函数本身具有相同签名的 operator() 的函数对象。您的 operator() 被硬编码为采用 int 并返回 void。
      • 感谢您添加对 Boost.Bind 的引用。有了它应该是可能的......
      【解决方案9】:

      如果你的编译器支持可变参数宏,我会试试这个:

      class Timer {
        Timer();// when created notes start time
        ~ Timer();// when destroyed notes end time, computes elapsed time 
      }
      
      #define TIME_MACRO(fn, ...) { Timer t; fn(_VA_ARGS_); } 
      

      所以,要使用它,你应该这样做:

      void test_me(int a, float b);
      
      TIME_MACRO(test_me(a,b));
      

      这是即兴的,您需要尝试让返回类型起作用(我认为您必须在 TIME_MACRO 调用中添加一个类型名称,然后让它生成一个临时变量)。

      【讨论】:

        【解决方案10】:

        我会这样做,使用函数指针而不是模板:

        // pointer to a function of the form:   double foo(int x);
        typedef double  (*MyFunc) (int);
        
        
        // your function
        double foo (int x) {
          // do something
          return 1.5 * x;
        }
        
        
        class Timer {
         public:
        
          Timer (MyFunc ptr)
            : m_ptr (ptr)
          { }
        
          double operator() (int x) {
            return m_ptr (x);
          }
        
         private:
          MyFunc m_ptr;
        };
        

        我将其更改为不引用函数,而只是一个普通的函数指针。用法保持不变:

          Timer t(&foo);
          // call function directly
          foo(i);
          // call it through the wrapper
          t(i);
        

        【讨论】:

        • 谢谢,但有一个问题:它不是通用的。您已经将它硬编码为使用一个接受 int 并返回 double 的函数。但是 Timer 应该是通用的,并且应该适用于任何功能。
        【解决方案11】:

        在 C++ 中,函数是一等公民,您可以将函数作为值传递。

        因为你希望它接受一个 int 并返回一个 double:

        Timer(double (*pt2Function)(int input)) {...
        

        【讨论】:

        • 不,我确实关心传递和返回值。 operator() 应该与原始函数具有相同的签名(取 int,在此特定示例的情况下返回 double。)
        猜你喜欢
        • 2021-04-19
        • 2021-04-06
        • 1970-01-01
        • 1970-01-01
        • 2016-07-21
        • 2015-10-27
        • 2020-04-09
        • 2021-05-08
        • 1970-01-01
        相关资源
        最近更新 更多