【问题标题】:How to implement decorators in C and C++如何在 C 和 C++ 中实现装饰器
【发布时间】:2011-01-12 09:48:48
【问题描述】:

我在 C 和 C++ 中都有一种情况,最好用 Python 之类的装饰器来解决它:我有几个函数我想用其他东西包裹起来,以便在函数进入之前语句被执行,当它离开时执行一些其他功能。

例如,我在一个库 C 文件中有一些函数,当调用它们时应该锁定一个信号量,并且在将控制权返回给被调用者之前,应该释放信号量。没有锁,它们具有以下结构:

int f1(int)
{
    ...
    ... 
}

int f2(char*)
{
    ....
}

int f3(blabla)
{
    ....
}

... fn(...)

我想定义一个全局信号量,它应该在每个函数被调用之前被锁定,并在函数返回时被释放。我想尽可能简单地做到这一点;接近这个的东西:

#lockprotected
int f1(int)
{
   ... /* nothing changed over here */
}
#endlockprotected

或类似的东西

int f1(int)
{
   ... /* nothing changed over here */
}
#lockprotected f1

我不想要的是:

  1. 更改函数名称,因为它们是库函数,并且可以从许多地方调用。
  2. 在返回调用之前明确放置任何语句,因为大多数函数在两者之间有许多早期返回。或者就此而言,更改函数的任何内部结构。

最优雅的方式是什么?

【问题讨论】:

  • 您必须决定 C 还是 C++,方法会有所不同,对于 C++,您可以使用 RAII,如下所述,对于 C,您必须将实现移动到单独的函数中,原件就变成了在调用实际实现时具有锁定/解锁功能的包装器 - 下面是@frunsi 答案的变体

标签: c++ c decorator


【解决方案1】:

使用 RAII(资源获取是初始化)来定义互斥锁上的锁。这将使您忘记第 2 点,即您不需要跟踪 return 语句来释放锁。

class Lock {
public:
    Lock () { // acquire the semaphore }
    ~Lock () { // release the semaphore }
}

接下来在你的函数开始处创建这个类的对象,即,

int f1 (int) {
    Lock l;
    // forget about release of this lock
    // as ~Lock() will take care of it
}

这样做的另一个好处是即使在从f1() 抛出异常的情况下,您仍然不必担心释放锁。所有堆栈对象在函数退出之前被销毁。

【讨论】:

  • +1 RAII 是 C++ 的实现方式。 f1 函数也必须被包装。这可以通过一个简单的模板函数来完成(会稍微混淆代码)或者像 frunsi 解释的那样创建简单的包装器。
  • 一个明显的问题是,您可能会保护太多的功能......而互斥锁通常不可重入。
  • 请注意Matthieu M.的建议并使用Reentrant Mutex。它允许单个线程多次获取互斥锁,同时阻止尝试调用库函数的其他线程。否则,只有一个线程存在自死锁的风险。
  • @daramarak,为什么我们需要包装 f1 函数?我不太明白。你能澄清一下吗?
  • @batbrat 防止更改 f1 功能。最好将锁定到包装函数的问题分开。此外,OP 指定“这里没有任何改变”,这可能意味着不应以某种方式触及该功能。
【解决方案2】:

如果您真的想要 C 解决方案,您可以使用以下宏:

#define LOCK   lock( &yourglobalsemaphore )
#define UNLOCK unlock( &yourglobalsemaphore )

#define LOCKED_FUNCTION_ARG1(TRet, FuncName, TArg1, Arg1Name ) \
TRet FuncName( TArg1 Arg1Name ) { \
 LOCK; \
 TRet ret = FuncName##_Locked( Arg1Name ); \
 UNLOCK; \
 return ret \
} \
TRet FuncName##_Locked(TArg1 Arg1Name )

#define LOCKED_FUNCTION_ARG2(TRet FuncName, TArg1, Arg1Name, TArg2, Arg2Name) \
//...etc

但是对于每个参数计数,您需要 1 个宏(并且该函数应该有一个返回类型)。

示例用法:

LOCKED_FUNCTION_ARG1(int, f1, int, myintarg)
{
   //unchanged code here
}

【讨论】:

  • FuncName_Locked 将与您键入的内容完全相同,因为它不是宏参数。您需要使用 ## 宏运算符连接 FuncName 和 _Locked。
  • 如果使用 C++,将它与 boost::preprocessor 结合起来将是一个很有前途的解决方案。
【解决方案3】:

像这样定义包装器:

class SemaphoreWrapper
{
private:
    semaphore &sem;
public
    SemaphoreWrapper(semaphore &s)
    {
        sem = s;
        sem.lock();
    }

    ~SemaphoreWrapper()
    {
        sem.unlock();
    }
}

然后在每个函数中简单地创建一个 SemaphoreWrapper 实例:

void func1()
{
    SemaphoreWrapper(global_semaphore);


    ...
}

SemaphoreWrapper 的构造函数和析构函数将负责锁定/解锁功能。

【讨论】:

    【解决方案4】:

    不要。

    你不能通过到处乱扔几把锁来从单线程转到多线程,然后寄希望于最好的结果。

    1. 您确定没有两个函数共享一个全局变量吗? C 函数因使用静态分配的缓冲区而臭名昭著。

    2. 互斥锁通常不可重入。因此,如果您装饰f1f2 并且其中一个调用另一个,您将陷入僵局。当然,您可以使用更昂贵的可重入互斥锁。

    在最好的情况下,多线程很难。它通常需要了解和调整执行流程。

    我很难想象扔几把锁会达到最好的效果。

    这显然忽略了这样一个事实,即如果函数没有考虑到 MT,它们可能会更慢(使用所有这些互斥操作),因此您不会获得太多好处。

    如果您真的需要您的信号量,请让客户端锁定它。他应该知道什么时候上锁,什么时候不上锁。

    【讨论】:

    • 虽然这偏离了主要对话,即 C 中的装饰器,但您的观点对于尝试通过放置一些锁将单线程库变成多线程库是非常有效的。
    • 该库是用 C 语言编写的,由 Python 绑定调用。它在 Python 的多线程中运行良好,因为 GIL(全局解释器锁)以非常高的效率解决了并发问题。我已经确定了一些需要同步的功能,而其他功能几乎无害。
    • 次要问题,但errno 作为每个线程的单独实例存在。
    • 哦,在句子 我很难想象扔几把锁会达到最好的效果你需要在第四个单词之后插入单词“on”: -)
    • @JeremyP:关于“开”,我想我会通过 ;)
    【解决方案5】:

    您可以编写包装函数,例如:

    int f1_locked(int x) {
      lock(..);
      int r=f1(x);
      unlock(..);
      return r;
    }
    

    使用预处理器可以节省一些工作。

    编辑

    正如有人所说,最好将实现移动到库内部函数中,并将包装器呈现给库用户,例如:

    // lib exports the wrapper:
    int f1(int x) {
      lock(..);
      int r=f1_unlocked(x);
      unlock(..);
      return r;
    }
    
    // for library internal use only:
    int f1_unlocked(int x) {
      ...
    }
    

    这还有一个额外的好处,即从 lib 内部函数调用和调用 lib 内部函数不需要多余的锁定(这可能与否,这取决于..),例如:

    void f2_unlocked() {
      ...
      f1_unlocked();
      ...
    }
    
    void f2() {
      lock();
      f2_unlocked();
      unlock();
    }
    

    【讨论】:

    • 他特别要求不要使用unlock(),因为这些函数中有很多return语句;问题中的第 2 点
    • @Jaywalker,此解决方案不会修改现有函数,只需 包装 它 - 因此只有一个返回点,这也适用于 C,但是它断点 1.
    • 需要一个包装函数,并调用 RAII 以防止显式调用解锁函数。一个简单的哨兵对象。
    • 查看我对预处理器详细信息的回答......这应该可以修复第 1 点
    • 引入锁后,函数也应该可以无缝调用。在这种情况下,所有调用者都必须将 f1() 更新为 f1_locked,这是不可行的。
    【解决方案6】:

    装饰器严格来说是一种语言特性,它为底层语义提供句法糖。您可以在 C++ 中获得的底层语义:只需适当地包装函数;你无法得到的语法糖。

    更好的选择是创建支持您的用例的适当代码设计,例如通过惯性和decorator pattern。这并不一定意味着类继承——该模式也可以使用类模板来实现。

    至于您的特定用例,已经发布了更好的替代方案。

    【讨论】:

      【解决方案7】:

      在我看来,您想要做的是Aspect Oriented Programming (AOP)。 C 和 C++ 中的 AOP 框架很少,但从几个月前我看到的情况来看,我认为 AspectC++ 项目提供了一个很好的 AOP 概念实现。不过我还没有在生产代码中测试过。

      【讨论】:

        猜你喜欢
        • 2013-02-25
        • 2021-12-01
        • 2015-03-12
        • 2010-10-31
        • 1970-01-01
        • 2016-04-08
        • 1970-01-01
        • 2014-10-29
        • 2017-04-08
        相关资源
        最近更新 更多