繁体   English   中英

函数钩在C ++中?

[英]Function hooking in C++?

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

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

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

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

我的问题是:

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

如果您正在讨论在函数体之前/之后调用新方法而不更改函数体,则可以将其基于 ,它使用自定义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;
}

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

另一种方法是使用类似Bjarne Stroustrup的函数包装技术。

回答你的第一个问题:

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

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

在C ++中模拟CLOS:before,:after和:around

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

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

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

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

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

class RunManager;
class PhysicsManager;
// ...

每个都定义了一组动作

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

它们是NOP,但是用户可以覆盖从Parent类派生的内容。

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM