【问题标题】:Avoiding ODR violations when using debug asserts使用调试断言时避免 ODR 违规
【发布时间】:2018-01-22 18:11:54
【问题描述】:

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

#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 在包含标头时独立指定其所需的断言设置?

【问题讨论】:

  • 为什么不使用MYDEBUG 作为转换运算符的编译时参数? T operator()(T value) { return do_conversion(value, MYDEBUG); },其中do_conversion 可能包含也可能不包含断言

标签: c++ one-definition-rule


【解决方案1】:

如果一个翻译单元在没有首先定义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。

【讨论】:

  • 不,仍然违反[basic.def.odr]/12.2。单个定义的想法是避免链接器必须折叠多个定义时出现问题,例如,如果函数是虚拟函数或通过指针调用。实际上只剩下一个版本了。
  • There can be more than one definition of a [...] inline function with external linkage, [...] non-static function template, [...] provided the definitions satisfy the following requirements.
  • @sp2danny - 当然。 operator() 仍然违反 ODR。
【解决方案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&lt;T, !MYDEBUG_FLAG&gt;,它也不会编译,因此不会违反 ODR(前提是每个 TU 中只定义了一个版本,无论是调试还是非调试) )。

【讨论】:

  • 我相信这段代码在技术上仍然违反了 ODR,因为模板参数的默认值不是定义的一部分。
  • 部分专业化应该可以解决这个问题,不是吗?
  • @Jarod42,应该,但我不确定你打算如何选择它?
  • @SergeyA:喜欢that 吗? (声明不同,但定义相同)。
  • @Jarod42 这很聪明,我看不出它为什么不起作用。愿意将此添加为答案吗?即使 OP 不喜欢它,我当然会赞成。
【解决方案3】:

第一个 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;
    }
};

【讨论】:

  • @RustyX:事实是用户在混合MYDEBUG 标志时必须处理Checker&lt;T, true&gt;/Checker&lt;T, false&gt; 的混合(我认为这是一个阻塞(无法解决)的问题).. .
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-03-18
  • 1970-01-01
  • 2020-04-03
  • 2020-04-29
  • 2018-11-23
  • 2017-11-03
  • 1970-01-01
相关资源
最近更新 更多