【问题标题】:Custom Compile Time Specifier For Debugging / Ignoring Functions用于调试/忽略函数的自定义编译时间说明符
【发布时间】:2020-03-06 11:15:17
【问题描述】:

实现以下调试功能的好方法是什么? 我想定义一个易于使用的函数说明符,类似于内联,它告诉编译器何时完全忽略基于启用的预处理器标志的函数。 理想情况下,我想声明这样的函数:

DEBUG_FUNCTION_VERBOSE void saveMesh(const std::string &fileName, const Mesh &mesh, ...);
DEBUG_FUNCTION_INFO void logInfo(const std::string &message);
DEBUG_FUNCTION_CHECK void checkVertexAttributes(const Mesh &mesh);
DEBUG_FUNCTION_ERROR void logError(const std::string &messageOfDoom);

有一个排序,从不太重要和缓慢到关键和更快。这个想法是我设置了一个预处理器标志,它确定启用了哪些特殊调试功能。例如,可能有一个像 DEBUG_LEVEL_CHECK 这样的标志来启用后两种类型的调试功能,或者另一个标志 DEBUG_LEVEL_VERBOSE 启用所有这些功能。 如果调试函数类型被禁用,我本质上希望编译器忽略对该函数的所有调用,包括对其参数的评估(如发布模式下的断言)。

这些功能的一个动机是加快调试速度。我想在我的代码中调用这样的调试函数并将它们留在那里。 为了控制输出和检查的内容,我宁愿使用 预处理器标志,而不是手动插入和删除对此类函数的调用。

另一个动机是在声明和调用这些函数时尽可能避免笨拙的预处理器保护。 而不是调用这样的函数:

#ifdef DEBUG_LEVEL_CHECK
checkVertexAttributes(mesh);
#endif // DEBUG_LEVEL_CHECK

我只是想正常调用函数:

checkVertexAttributes(mesh);

理想情况下,此调试方案的实现应具有以下属性: 首先,每个函数的代码中应该只有一个点,我必须声明它的调试级别(可能类似于添加像 inline 关键字这样的说明符)。其次,我只想定义一次调试级逻辑,因为它总是相同的。第三,这些调试函数具有任意上下文(属于某个类、某个命名空间或...),我可能想稍后添加这些函数。这意味着哪些函数是调试函数应该独立于调试级别逻辑本身的实现。我不想在这个地方列出所有使用的调试函数来膨胀这个逻辑。第四,如果一个函数被禁用,不仅它的执行本身而且它的参数的评估都应该被跳过(就像 assert 一样)。最后,我想方便地调用仅依赖于该逻辑的调试函数,但以透明的方式进行,如上图所示。

我看到了这个帖子: What exactly happens to empty inline functions? 但在我的情况下,这似乎会导致冗余代码,我不确定您将如何将其应用于共享相同名称的重载函数。

【问题讨论】:

  • 使用普通的#ifdef 子句还不够吗?正如您链接的答案明确指出,如果相应的#define 存在,它保证该行被完全忽略。
  • 不,我想知道一些更简单、更透明的东西。请查看我更新的问题。
  • 您的问题未更新。无论如何,最简单和最透明的更多是一种意见,因此并不适合 SO。通过使用我的答案,您至少可以获得所需的行为,而无需在多个地方更新代码。似乎和 IMO 一样容易。当然,函数签名不会显示该函数只是调试功能,但您可以在此处添加注释。这就是您寻找替代方案的原因吗?

标签: c++ debugging preprocessor compile-time


【解决方案1】:

类似:

inline void saveMesh(...) {
    #ifdef DEBUG
    ...
    #else
    return;
 }

如果未定义DEBUG,则对该函数的任何调用都不会执行任何操作,编译器可能会对其进行完全优化。这是你要找的吗?

【讨论】:

  • 我不确定这是否是个好主意。一个问题是调试函数可能很长。在这种情况下,我不希望在头文件中嵌入这么长的调试函数。这反过来意味着我总是需要它们的两个版本。如果已停用,则为空的内联函数;如果启用了相应的预处理器标志,则为正常的调试版本。此外,该方案的相同实现将针对具有相同调试级别的每个函数重复发生。我想避免这种冗余,并正在寻找一种更智能的方法。
【解决方案2】:

您可以让编译器通过可变参数宏吞下任何函数调用。

void test(int, int) {}
void test(int) {}

#define test(...)

int main()
{
    test(78, 12);
    test(12);
    test(swallows literally anything);
}

至于关卡,你可以把一些#ifdefs放在一起,为它做一个简单的工作系统。

#ifdef DEBUG_FUNCTION_VERBOSE
#define DEBUG_FUNCTION_INFO
#else
#define saveMesh(...)
#endif

#ifdef DEBUG_FUNCTION_INFO
#define DEBUG_FUNCTION_CHECK
#else
#define logInfo(...)
#endif

#ifdef DEBUG_FUNCTION_CHECK
#define DEBUG_FUNCTION_ERROR
#else
#define checkVertexAttributes(...)
#endif

#ifndef DEBUG_FUNCTION_ERROR
#define logError(...)
#endif

【讨论】:

  • 请注意,对于像test(function_with_side_effect()) 这样的调用(与assert 相同),剩余程序的行为可能会发生变化。
  • @Jarod42 你的意思是副作用不会发生变化?
  • 是的,assert 也会发生,所以 assert(db.open() == success) 这样的代码是错误的。
  • 使用可变参数宏让编译器忽略函数是很好的。虽然这个解决方案在我看来就像假设我事先已经知道我的所有调试函数,以便我可以将它们的可变参数宏收集在一个代码块中,就像你建议的那样。但是如果我想添加这样的调试函数而不需要将它们放在一个这样的地方(以避免经常包含头文件并重新编译相当多的依赖它的源文件)怎么办。调试级别的逻辑并没有改变,最好将其封装在一个地方并将其放在一个通用的调试头中。
  • @Samir 好吧,看看您更新的具体要求,您基本上是在描述一种不存在的语言功能。 c++ 中没有自定义函数说明符,也没有你想要的。您可能更容易回答哪些需要妥协的要求。
【解决方案3】:

我妥协了一些要求并提出了这个:

#include <functional>

enum class Debug
{
    Verbose = 0,
    Info = 1,
    Check = 2,
    Error = 3
};

#define DebugLevelApp Debug::Verbose


template <Debug functionLevel, typename Function, typename... Args>
inline void debugCall(Function&& func, Args&&... args)
{
    if constexpr (DebugLevelApp <= functionLevel)
    {
        auto function = std::bind(std::forward<Function>(func), std::forward<Args>(args)...);
        function();
    }
}

int main()
{
    debugCall<Debug::Verbose>(debug1, sideEffect());
    debugCall<Debug::Verbose>(debug2, sideEffect(), sideEffect());
    debugCall<Debug::Info>(debug2, sideEffect(), sideEffect());
    return 0;
}

这里的缺点是所有参数都被评估,即使相应的函数没有被调用。这几乎看起来像是不受欢迎的令人惊讶的行为。

所以我把调试方案的实现改成这样:

enum class Debug
{
    Verbose = 0,
    Info = 1,
    Check = 2,
    Error = 3
};

#ifndef DEBUG_LEVEL_APP
    #define DEBUG_LEVEL_APP Debug::Info
#endif // DEBUG_LEVEL_APP

#define DEBUG_CALL(CALL_DEBUG_LEVEL) if constexpr (APP_DEBUG_LEVEL <= CALL_DEBUG_LEVEL)

如果我想要调试函数调用,我可以使用预处理器标志 DEBUG_LEVEL_APP 轻松排除这些调用,我以这种方式标记此类调用:

DEBUG_CALL(Debug::Verbose)
debug2(sideEffect(), sideEffect());
DEBUG_CALL(Debug::Info)
debug2(sideEffect(), sideEffect());
DEBUG_CALL(Debug::Info)
debug1(sideEffect());

这里没有被调用的函数没有副作用。这个实现比我最初想要的更明确,但最终可能会更好。不过,我仍然愿意就如何改进此方案提出建议。

【讨论】:

    猜你喜欢
    • 2022-01-14
    • 1970-01-01
    • 1970-01-01
    • 2015-12-07
    • 2011-02-26
    • 2016-01-15
    • 1970-01-01
    • 1970-01-01
    • 2022-10-04
    相关资源
    最近更新 更多