函数钩在C ++中?

时间:2011-10-12 17:03:04

标签: c++

“挂钩”是指非侵入性地覆盖函数行为的能力。一些例子:

  • 在功能正文之前和/或之后打印日志消息。
  • 将函数体包裹在try catch体中。
  • 测量功能的持续时间
  • 等...

我在各种编程语言和库中看到了不同的实现:

  • 面向方面编程
  • JavaScript的第一类函数
  • OOP装饰图案
  • WinAPI子类化
  • Ruby的method_missing
  • SWIG%exception关键字用于包装try / catch块中的所有函数,可以(ab)用于挂钩

我的问题是:

  • IMO这是一个非常有用的功能,我想知道为什么它从未被实现为C ++语言功能。是否有任何理由阻止这种情况发生?
  • 在C ++程序中实现此功能的推荐技术或库是什么?

7 个答案:

答案 0 :(得分:13)

如果您正在讨论在函数体之前/之后调用新方法而不更改函数体,则可以将其基于this,它使用自定义shared_ptr删除器触发后身功能。它不能用于try/catch,因为之前和之后需要使用这种技术的单独函数。

此外,下面的版本使用shared_ptr,但使用C ++ 11,您应该能够使用unique_ptr获得相同的效果,而无需每次创建和销毁共享指针的成本用它。

#include <iostream>
#include <boost/chrono/chrono.hpp>
#include <boost/chrono/system_clocks.hpp>
#include <boost/shared_ptr.hpp>

template <typename T, typename Derived>
class base_wrapper
{
protected:
  typedef T wrapped_type;

  Derived* self() {
    return static_cast<Derived*>(this);
  }

  wrapped_type* p;

  struct suffix_wrapper
  {
    Derived* d;
    suffix_wrapper(Derived* d): d(d) {};
    void operator()(wrapped_type* p)
    {
      d->suffix(p);
    }
  };
public:
  explicit base_wrapper(wrapped_type* p) :  p(p) {};


  void prefix(wrapped_type* p) {
     // Default does nothing
  };

  void suffix(wrapped_type* p) {
     // Default does nothing
  }

  boost::shared_ptr<wrapped_type> operator->() 
  {
    self()->prefix(p);
    return boost::shared_ptr<wrapped_type>(p,suffix_wrapper(self()));
  }
};




template<typename T>
class timing_wrapper : public base_wrapper< T, timing_wrapper<T> >
{
  typedef  base_wrapper< T, timing_wrapper<T> > base;
  typedef boost::chrono::time_point<boost::chrono::system_clock, boost::chrono::duration<double> > time_point;

  time_point begin;
public:
  timing_wrapper(T* p): base(p) {}


  void prefix(T* p) 
  {
    begin = boost::chrono::system_clock::now();
  }

  void suffix(T* p)
  {
    time_point end = boost::chrono::system_clock::now();

    std::cout << "Time: " << (end-begin).count() << std::endl;
  }
};

template <typename T>
class logging_wrapper : public base_wrapper< T, logging_wrapper<T> >
{
  typedef  base_wrapper< T, logging_wrapper<T> > base;
public:
  logging_wrapper(T* p): base(p) {}

  void prefix(T* p) 
  {
    std::cout << "entering" << std::endl;
  }

  void suffix(T* p) 
  {
    std::cout << "exiting" << std::endl;
  }

};


template <template <typename> class wrapper, typename T> 
wrapper<T> make_wrapper(T* p) 
{
  return wrapper<T>(p);
}


class X 
{
public:
  void f()  const
  {
    sleep(1);
  }

  void g() const
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }

};



int main () {

  X x1;


  make_wrapper<timing_wrapper>(&x1)->f();

  make_wrapper<logging_wrapper>(&x1)->g();
  return 0;
}

答案 1 :(得分:5)

您可以利用特定于编译器的功能,例如GCC的-finstrument-functions。其他编译器可能具有类似的功能。有关其他详细信息,请参阅此SO question

另一种方法是使用类似Bjarne Stroustrup's function wrapping技术的东西。

答案 2 :(得分:4)

回答你的第一个问题:

  • 大多数动态语言都有method_missing个结构,PHP有magic methods__call__callStatic),Python有__getattr__。我认为这在C ++中不可用,它违背了C ++的类型性质。在类上实现它意味着任何拼写错误最终都会调用此函数(在运行时!),这可以防止在编译时捕获这些问题。将C ++与鸭子打字混合似乎不是一个好主意。
  • C ++试图尽可能快,所以第一类函数是不可能的。
  • AOP。现在这更有趣了,技术上没有任何东西可以阻止它被添加到C ++标准中(除了为已经非常复杂的标准添加另一层复杂性这一事实可能不是一个好主意)。事实上,有些编译器可以挥动代码,AspectC++就是其中之一。一年前左右它不稳定但看起来从那时起他们设法用一个相当不错的测试套件发布1.0,所以它现在可以完成这项工作。

有几种技巧,这是一个相关的问题:

Emulating CLOS :before, :after, and :around in C++

答案 3 :(得分:2)

  

IMO这是一个非常有用的功能,为什么它不是C ++语言功能?是否有任何理由阻止这种情况发生?

C ++语言没有提供直接这样做的任何方法。但是,它也没有对此(AFAIK)构成任何直接约束。这种类型的功能在解释器中比在本机代码中更容易实现,因为解释是一个软件,而不是CPU流机器指令。如果你愿意,你可以提供一个支持钩子的C ++解释器。

问题是为什么人们使用C ++。很多人都在使用C ++,因为他们想要纯粹的执行速度。为了实现这一目标,编译器以操作系统的首选格式输出本机代码,并尝试在编译的可执行文件中硬编码。最后一部分通常意味着在编译/链接时计算地址。如果你在那时修复一个函数的地址(或者更糟糕的是,内联函数体),那么就不再支持钩子了。

话虽这么说, 方法可以使钩子变得便宜,但它需要编译器扩展并且完全不可移植。 Raymond Chen在博客中介绍了如何在Windows API中实现热补丁。他还建议不要在常规代码中使用它。

答案 4 :(得分:1)

至少在我使用的c ++框架上提供了一组纯虚拟类

class RunManager;
class PhysicsManager;
// ...

每个都定义了一组动作

void PreRunAction();
void RunStartAction()
void RunStopAction();
void PostRunAction();

是NOP,但是用户可以覆盖从Parent类派生的地方。

将它与条件编译相结合(是的,我知道“Yuk!”),你可以得到你想要的东西。

答案 5 :(得分:0)

这不是C ++的事情,但为了完成你提到的一些事情,我在* nix系统中使用了LD_PRELOAD环境变量。这个技术的一个很好的例子是faketime库,它挂钩了时间函数。

答案 6 :(得分:0)

  1. 必须有一种方法来实现功能,而不会影响不使用该功能的代码的性能。 C ++的设计原则是您只需为所使用的功能支付性能成本。如果检查每个函数以检查它是否被覆盖,那么插入对于许多C ++项目来说都会慢得令人无法接受。特别是,使其工作,以便没有性能成本,同时仍然允许独立编译被覆盖和覆盖的功能将是棘手的。 如果你只允许编译时覆盖,那么性能更容易(链接器可以处理覆盖地址),但是你要比较ruby和javascript,它可以让你在运行时改变这些东西

  2. 因为它会破坏类型系统。如果有人可以覆盖其行为,那么函数是私有还是非虚函数意味着什么呢?

  3. 可读性会受到很大影响。任何函数都可能在代码中的其他位置覆盖其行为!理解函数功能所需的上下文越多,找出大型代码库就越困难。 挂钩是一个错误,而不是一个功能。至少如果能够阅读你几个月后写的内容是一项要求。