在C ++中,如何防止函数被递归调用

时间:2010-02-04 15:49:56

标签: c++

我有一个在堆上使用内存的函数,如果在完成同一函数的另一个实例之前调用它,则会出现严重错误。 如何防止在编译时发生这种情况?

12 个答案:

答案 0 :(得分:9)

你的问题不清楚,你的意思是在单线程场景(递归或相互递归)还是多线程场景(重新入门)?

在多线程场景中,没有办法在编译时阻止这种情况,因为编译器没有线程的知识。

在单线程场景中,除了使用你的大脑之外,我不知道在编译时阻止递归调用的方法。只要您可以分析控制流并证明您的函数不会调用自身并且它调用的函数都没有调用它,那么您应该是安全的。

答案 1 :(得分:8)

在编译时以任何数量的确定性检测递归将是非常困难的。一些静态代码分析工具可能会这样做,但即使这样,您也可以进入涉及代码分析器无法检测的线程的运行时方案。

您需要在运行时检测递归。从根本上说,这样做非常简单:

bool MyFnSimple()
{
    static bool entered = false;
    if( entered )
    {
        cout << "Re-entered function!" << endl;
        return false;
    }
    entered = true;

    // ...

    entered = false;
    return true;
}

当然,最大的问题是它不是线程安全的。有几种方法可以使线程安全,最简单的方法是使用临界区并阻止第二个条目直到第一个条目离开。 Windows代码(不包括错误处理):

bool MyFnCritSecBlocking()
{
    static HANDLE cs = CreateMutex(0, 0, 0);
    WaitForSingleObject(cs, INFINITE);
    // ... do stuff
    ReleaseMutex(cs);
    return true;
}

如果你希望函数在重新输入函数时返回错误,你可以在获取之前先测试一下这个错误:

bool MyFnCritSecNonBlocking()
{
    static HANDLE cs = CreateMutex(0, 0, 0);
    DWORD ret = WaitForSingleObject(cs, 0);
    if( WAIT_TIMEOUT == ret )
        return false;   // someone's already in here
    // ... do stuff
    ReleaseMutex(cs);
    return true;
}

除了使用静态布尔和暴击之外,可能还有无限的方法来使这只猫皮肤。我想到的是将本地值与Windows中的一个Interlocked函数进行测试的组合:

bool MyFnInterlocked()
{
    static LONG volatile entered = 0;
    LONG ret = InterlockedCompareExchange(&entered, 1, 0);
    if( ret == 1 )
        return false;   // someone's already in here
    // ... do stuff
    InterlockedExchange(&entered, 0);
    return false;
}

当然,你必须考虑异常安全和死锁。您不希望函数出现故障,使任何代码无法进入。您可以在RAII中包含上面的任何构造,以确保在函数中发生异常或提前退出时释放锁。

UPDATE:

在阅读完评论后,我意识到我可以包含说明如何实现RAII解决方案的代码,因为您编写的任何实际代码都将使用RAII来处理错误。这是一个简单的RAII实现,它还说明了在出现问题时运行时会发生什么:

#include <windows.h>
#include <cstdlib>
#include <stdexcept>
#include <iostream>

class CritSecLock
{
public:
    CritSecLock(HANDLE cs) : cs_(cs)
    {
        DWORD ret = WaitForSingleObject(cs_, INFINITE);
        if( ret != WAIT_OBJECT_0 ) 
            throw std::runtime_error("Unable To Acquire Mutex");
        std::cout << "Locked" << std::endl;
    }
    ~CritSecLock()
    {
        std::cout << "Unlocked" << std::endl;
        ReleaseMutex(cs_);
    }
private:
    HANDLE cs_;
};

bool MyFnPrimitiveRAII()
{
    static HANDLE cs = CreateMutex(0, 0, 0);
    try
    {
        CritSecLock lock(cs);
        // ... do stuff
        throw std::runtime_error("kerflewy!");
        return true;
    }
    catch(...)
    {
        // something went wrong 
        // either with the CritSecLock instantiation
        // or with the 'do stuff' code
        std::cout << "ErrorDetected" << std::endl;
        return false;
    }
}

int main()
{
    MyFnPrimitiveRAII();
    return 0;
}

答案 2 :(得分:6)

如果没有静态分析,则无法在编译时执行此操作。这是一个异常安全的递归断言:

#include <cassert>

class simple_lock
{
public:
    simple_lock(bool& pLock):
    mLock(pLock)
    {
        assert(!mLock && "recursive call");
        mLock = true;
    }

    ~simple_lock(void)
    {
        mLock = false;
    }

private:
    simple_lock(const simple_lock&);
    simple_lock& operator=(const simple_lock&);

    bool& mLock;
};

#define ASSERT_RECURSION static bool _lockFlag = false; \
                            simple_lock _lock(_lockFlag)

void foo(void)
{
    ASSERT_RECURSION;

    foo();
}

int main(void)
{
    foo();
    //foo();
}

答案 3 :(得分:5)

如果没有某种静态分析器,则无法在编译时执行此操作。但是,对此进行简单的运行时检查将起作用:

注意:为防止多线程并发但非递归调用,您需要更强大的功能。

void myFunc() {
  static int locked = 0;
  if (locked++)
  {
    printf("recursion detected\n!");
  }

  ....

  locked--;
}

注意:您应将此功能放在.c.cc文件中,而不是标题中。

如果您确实有多线程,我建议您使用pthread锁来控制对它引用的共享变量的访问。

答案 4 :(得分:4)

这个问题在任何图灵完整语言中都是不可判定的。我无法证明这一点。我才知道。

答案 5 :(得分:2)

您最好的选择是使用互斥锁。你可以使用信号量,但我个人更喜欢这里的互斥量。这将允许您阻止其他线程调用它。

*编辑*

你希望它在编译时发生吗?

你在我的朋友身边隐藏着什么。

答案 6 :(得分:2)

函数调用堆栈是在运行时创建的,在编译时你只能检查你的函数本身是否是递归的,即它自己调用了吗?。

答案 7 :(得分:2)

c++-faq-lite在类似的情况下有一些很好的建议:写下你在做这样事情时会遇到问题的评论:

// We'll fire you if you try recursion here

我还没看过这个建议是否也适用于递归

答案 8 :(得分:1)

你不能在编译时完成它,你需要按照整个应用程序的完整控制流来完成这个(如果函数调用另一个调用另一个函数的函数调用另一个函数怎么办呢?再次调用原始函数...)。

在思考时代替。在函数文档中添加一个巨大的注释。另外,可能是其他响应提供的运行时解决方案之一 - 用于调试版本。

只是添加另一个:使用静态互斥锁来保护函数体(boost的scoped_lock会大大简化它)。

答案 9 :(得分:1)

这个编译

#include <stdio.h>

int testFunc() {
#define testFunc
printf("Ok\n");
}
#undef testFunc
int main() { testFunc(); }

这不是

#include <stdio.h>

int testFunc() {
#define testFunc
printf("Ok\n");
testFunc();
}
#undef testFunc
int main() { testFunc(); }

错误:test.c:7: error: expected expression before ‘)’ token

它也适用于多功能递归:

#include <stdio.h>

int testFunc1() {
#define testFunc1
printf("1\n");
testFunc2();
}

int testFunc2() {
#define testFunc2
printf("2\n");
//uncomment to cause error: `test.c:13: error: expected expression before ‘)’ token`
//testFunc1();
}

#undef testFunc1
#undef testFunc2

int main() { testFunc1(); }

答案 10 :(得分:1)

这是我使用RAII防止单线程再入的解决方案:

    struct NestingTracker
    {
        NestingTracker(uint32_t &cnt) : _cnt(cnt) { ++_cnt; }
        ~NestingTracker()                         { assert(_cnt); --_cnt; }
        uint32_t& _cnt;
    };

#define NO_REENTRY(x)                        \
    static uint32_t cur_nesting = 0;         \
    if (cur_nesting)                         \
        return (x);                             \
    NestingTracker nesting_track(cur_nesting)

用作

void f(...)
{
    NO_REENTRY(void());
    ...
}

答案 11 :(得分:0)

执行此操作的唯一方法是确保客户端代码无法引用您不想重新输入的功能。这可以通过创建函数static或匿名命名空间或类似技术来实现。

不幸的是,为了确保工作正常,从main到您的函数的所有函数也必须以这种方式声明,并且这种情况变得非常难以快速管理。

而且,虽然C ++在技术上说从代码中调用main是未定义的,你不应该这样做,但大多数实现都会很乐意为你递归mainmain必须有一个可以从程序中的任何其他位置访问的名称。

所以,实际上它实际上是不可能的。

我希望您在编写时尽量让编译器给您一个错误,而不是仅仅确保简单的方法没有任何可能的方法。