我有一个仅标头的库,在调试模式下编译时启用了一些额外的快速失败运行时断言。标题的简化版本如下所示:
#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
后包含标头,并且我将生成的目标文件链接在一起,这是否会构成 ODR 违规?MYDEBUG
是的,这违反了单一定义规则。这违反了内联函数的规则,即内联函数定义必须在所有翻译单元中具有精确的标记。
如何避免这种情况,但仍然允许每个 TU 在包含标头时独立指定其所需的断言设置?
一种处理方法是将
MYASSERT
static
函数。#ifdef MYDEBUG
static void MYASSERT(bool condition)
{
if (!(condition))
{
std::terminate();
}
}
#else
static void MYASSERT(bool condition)
{
// Noop
}
#endif
看来你不能。谢谢,@RustyX。
解决方案 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 中只定义了一个版本,无论是调试版本还是非调试版本)。
第一个 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;
}
};
嗯,您可以通过首先确保该函数未使用 ODR 来实现。这意味着您必须确保该函数始终是内联的。
这样做的一个前提是,从标准角度来看它是一个内联函数。你的函数已经满足这个了。
根据编译器和编译器标志,您可能还需要函数的特殊属性以强制其内联。对于 gcc,您可以使用
[[gnu::always_inline]]
。
您绝不能存储函数的地址
如果我们忽略可以获取任何内容的地址这一事实,那么这是一种非常有用的技术,可以确保调用者按照应有的方式行事。
#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问题。