使用调试断言时避免 ODR 违规

问题描述 投票:0回答:4

我有一个仅标头的库,在调试模式下编译时启用了一些额外的快速失败运行时断言。标题的简化版本如下所示:

#include <exception>

#ifdef MYDEBUG
#   define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)
#else
#   define MYASSERT(condition)
#endif

template<typename T>
class Checker
{
public:

    T operator()(T value)
    {
        MYASSERT(value);
        return value;
    }
};

如果一个翻译单元在没有先定义

MYDEBUG
的情况下包含标头,而另一个翻译单元在定义
MYDEBUG
后包含标头,并且我将生成的目标文件链接在一起,这是否会构成 ODR 违规?

如何避免这种情况,但仍然允许每个 TU 在包含标头时独立指定其所需的断言设置?

c++ one-definition-rule
4个回答
3
投票

如果一个翻译单元在没有先定义

MYDEBUG
的情况下包含标头,而另一个翻译单元在定义
MYDEBUG
后包含标头,并且我将生成的目标文件链接在一起,这是否会构成 ODR 违规?

是的,这违反了单一定义规则。这违反了内联函数的规则,即内联函数定义必须在所有翻译单元中具有精确的标记。

如何避免这种情况,但仍然允许每个 TU 在包含标头时独立指定其所需的断言设置?

一种处理方法是将

MYASSERT
定义为文件范围的
static
函数。

#ifdef MYDEBUG
static void MYASSERT(bool condition)
{
   if (!(condition))
   {
      std::terminate();
   }
}
#else
static void MYASSERT(bool condition)
{
   // Noop
}
#endif

看来你不能。谢谢,@RustyX。


2
投票

解决方案 1: 使用范围界定:

#ifdef MYDEBUG
#   define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)
#else
#   define MYASSERT(condition)
#endif

namespace {
  template<typename T>
  class Checker
  {
  public:
      T operator()(T value)
      {
          MYASSERT(value);
          return value;
      }
  };
}

这本质上改变了

Checker
到内部链接,并且可能会带来额外的成本,也就是说它可能会多次出现在最终的可执行文件中。然而,在这种特殊情况下,没有额外的成本,因为它可能会被内联。

解决方案2:在调试模式下参数化模板:

更新3:感谢@Jarod42的建议,使用模板专业化

#ifdef MYDEBUG
#   define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)
#   define MYDEBUG_FLAG true
#else
#   define MYASSERT(condition)
#   define MYDEBUG_FLAG false
#endif

template<typename T, bool = MYDEBUG_FLAG> class Checker;

template<typename T>
class Checker<T, MYDEBUG_FLAG>
{
public:
    T operator()(T value)
    {
        MYASSERT(value);
        return value;
    }
};

那么调试实例化和非调试实例化将彼此独立。

这样做的好处是,即使有人意外实例化

Checker<T, !MYDEBUG_FLAG>
,它也不会编译,因此不会违反 ODR(假设每个 TU 中只定义了一个版本,无论是调试版本还是非调试版本)。


0
投票

第一个 RustyX 答案的变体,但我认为已修复:

#ifdef MYDEBUG
#   define MYDEBUG_FLAG true
#else
#   define MYDEBUG_FLAG false
#endif

#define MYASSERT(condition) do{ if (!(condition)) std::terminate(); } while(0)

// Following declaration differs, but doesn't break ODR.
template<typename T, bool = MYDEBUG_FLAG> class Checker;

// And both definitions of specialization.
template <typename T>
class Checker<T, true>
{
public:

    T operator()(T value)
    {
        MYASSERT(value);
        return value;
    }
};

template <typename T>
class Checker<T, false>
{
public:

    T operator()(T value)
    {
        return value;
    }
};

0
投票

嗯,您可以通过首先确保该函数未使用 ODR 来实现。这意味着您必须确保该函数始终是内联的。

  1. 这样做的一个前提是,从标准角度来看它是一个内联函数。你的函数已经满足这个了。

  2. 根据编译器和编译器标志,您可能还需要函数的特殊属性以强制其内联。对于 gcc,您可以使用

    [[gnu::always_inline]]

  3. 您绝不能存储函数的地址

如果我们忽略可以获取任何内容的地址这一事实,那么这是一种非常有用的技术,可以确保调用者按照应有的方式行事。

#include <cassert>

void do_work(int);

[[gnu::always_inline]] inline void do_work_checked(int value)
{
    assert(value);
    do_work(value);
}

void foo(int val)
{
    do_work_checked(val);
}

即使没有优化,

do_work_checked
也不会出现在装配体中。

为了防止(3),一种方法是使用宏

#include <cassert>

void do_work(int);

#define do_work_checked(value) \
{ \
   assert(value); \
   do_work(value); \
}

void foo(int val)
{
    do_work_checked(val);
}

由于不再有任何功能,所以不可能有任何ODR问题。

© www.soinside.com 2019 - 2024. All rights reserved.