【问题标题】:Getting rid of #ifndef NDEBUG摆脱#ifndef NDEBUG
【发布时间】:2011-02-16 13:36:11
【问题描述】:

我的大多数类都有调试变量,这使得它们通常看起来像这样:

class A
{
    // stuff
#ifndef NDEBUG
    int check = 0;
#endif
};

方法可能如下所示:

for (/* big loop */) {
    // code
#ifndef NDEBUG
    check += x;
#endif
}

assert(check == 100);

没有什么比#ifndef NDEBUG 更丑的了。不幸的是,我知道没有编译器可以在没有这些 #ifndef 的情况下优化 check 变量(我不知道这是否允许)。

所以我试图想出一个解决方案,让我的生活更轻松。这是它现在的样子:

#ifndef NDEBUG

#define DEBUG_VAR(T) T

#else

template <typename T>
struct nullclass {
    inline void operator+=(const T&) const {}
    inline const nullclass<T>& operator+(const T&) const { return *this; }
    // more no-op operators...
};

#define DEBUG_VAR(T) nullclass<T>

#endif

所以在调试模式下,DEBUG_VAR(T) 只会生成一个 T。否则它会生成一个只有无操作的“空类”。我的代码看起来像这样:

class A {
   // stuff
   DEBUG_VAR(int) check;
};

然后我可以像使用普通变量一样使用 check !惊人的!但是,我仍然无法解决 2 个问题:

1。它仅适用于 int、float 等。

“空类”没有 push_back() 等。没什么大不了的。无论如何,大多数调试变量都是整数。

2。 “空类”是 1 个字符宽!!

C++ 中的每个类至少有 1 个字符宽。因此,即使在发布模式下,使用 N 个调试变量的类至少也会有 N 个字符太大。这在我眼里简直是不能接受的。这违反了我尽可能追求的零开销原则。

那么,我该如何解决第二个问题? 是否有可能在不影响非调试模式下的性能的情况下摆脱#ifndef NDEBUG?我接受任何好的解决方案,即使它是你最黑暗的 C++ 魔法或 C++0x。

【问题讨论】:

  • 欢迎来到 SO!我对第一个措辞很好且有趣的问题表示赞赏:D
  • 您应该在这些变量的名称中添加一个调试前缀(例如debug_ 或简短的dbg_),否则您将无法分辨哪个变量是调试变量,哪个不是调试变量正在查看源文件,现在您没有 #ifndef NDEBUG 行了。
  • 在调试/发布模式下拥有不同的对象布局是一个等待调试的艰难时期。我发现在发布模式下保持调试检查和断言的好习惯,并在分析和全面调试后禁用它们。如果您的发布模式与调试模式仅在优化设置上有所不同,那您就可以了。特别要注意 Visual Studio 中讨厌的 _SECURE_SCL 和 _HAS_ITERATOR_DEBUGGING,除非我真的需要它们,否则我总是禁用它们。

标签: c++ debugging metaprogramming


【解决方案1】:

怎么样:

#ifndef NDEBUG
#define DEBUG_VAR(T) static nullclass<T>
#endif

现在不会向使用DEBUG_VAR(T) 作为成员的类添加额外的存储空间,但声明的标识符仍然可以像成员一样使用。

【讨论】:

  • 我不知道解决方案会这么简单。就像 Martin Stone 提到的那样,您仍然需要在某个地方定义它的一个实例,但这只是一个小麻烦。
  • @aschepler: 如果每个对象而不是每个类都需要这样的调试变量呢?是的,我看到了这个例子,但问题不是更广泛吗?我个人的建议是阅读@VJo 答案和@Alexandre C. 评论
  • @Andy T 不,这个解决方案的天才之处在于 nullclass 实例可以是静态的,因为它只执行无操作。普通的 T 实例(在调试模式下)仍然是非静态的。
【解决方案2】:

您无法解决第二个问题,因为 c++ 标准要求类或对象的 sizeof 至少为一个字节。

最简单的解决方案是不引入此类 hack,并正确地对您的代码进行单元测试。

【讨论】:

  • 按照同样的逻辑,断言是黑客——你应该正确地对你的代码进行单元测试......对吗?
  • @zeuxcg,断言对业务逻辑很重要的事情是好的。引入新的仅测试变量以便您可以断言它们是不好的。这是一个微妙但重要的区别
  • @Glen,有时有些复杂的假设需要额外的数据来验证。在其他时候,调试数据是为了帮助调试过程——例如,在发布时是多余的但在调试时非常有用的对象名称。
  • +1,但是第二个问题有一个“空基类优化”形式的解决方案:只需将调试变量移动到基类中,并使用(多)继承。如果后一个基类为空,则可以对其进行优化。这是一个提醒在存在多重继承时始终使用 static_cast 而不是 C 样式转换的好地方。
  • @Alexandre:您如何将check 之类的标识符与每个空基类关联起来?
【解决方案3】:

这样的事情可能会起作用:

#ifdef NDEBUG
    #define DEBUG_VAR(type, name)
    #define DEBUG_VAR_OP(code)
#else
    #define DEBUG_VAR(type, name) type name;
    #define DEBUG_VAR_OP(code) code;
#endif

使用示例:

struct Foo
{
    DEBUG_VAR(int, count)
};

void bar(Foo* f)
{
    DEBUG_VAR_OP(f->count = 45)
}

但是,请注意,一般来说,您的程序的不同配置之间的内存布局差异越大,您将要解决的错误就越多(“它在调试中有效,但在发布时随机崩溃”)得到。所以如果你发现自己经常使用额外的调试数据,你应该重新设计你的数据结构。当有大量调试数据时,最好在发布模式下留下指向调试数据的指针(即struct Foo { ... ; struct FooDebug* debugData; /* NULL in Release */ };

【讨论】:

  • 好吧,DEBUG_VAR_OP 只是#ifndef NDEBUG 的一种较短形式。不过,我实际上可能会使用它,因为它不会像 #ifndef 那样弄乱我的缩进。
  • 是的,唯一的好处是它是单线的。
  • +1 我更喜欢这个解决方案而不是接受的答案。当然,它没有那么时尚和“现代 c++-ish”,但它有几个好处。首先,它也解决了OP的问题1)。其次,它向所有阅读代码的人清楚地表明,DEBUG_VAR_OP 中的任何内容都只能在调试模式下执行。
  • 顺便说一句:我认为会删除 ;在 DEBUG_VAR_OP 宏定义中的代码之后强制宏的用户自己提供它。通常与尝试格式化源代码的工具配合使用会更好。也许将整个事情包装成“do { code; } while(0)”。
【解决方案4】:

如何在调试模式下声明成员对象为静态:

#define DEBUG_VAR(T) static nullclass<T>

您必须在某处定义每个对象的实例。

(顺便说一句,空类的对象必须占用空间的原因是它们可以拥有唯一的this指针。)

编辑:删除了第二个想法——不起作用。

【讨论】:

    猜你喜欢
    • 2010-11-03
    • 2012-08-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-23
    • 2017-02-03
    • 2015-11-21
    • 1970-01-01
    相关资源
    最近更新 更多