我们发现了一种在编译时强制执行一段代码只能实例化一次的方法。总之,对于相关代码的每个实例化,我们声明一个具有不同返回类型的函数。如果您尝试实例化代码,则编译将失败并出现以下错误:
error: functions that differ only in their return type cannot be overloaded
尽管这个解决方案很聪明,但它也有一些问题:
- 错误消息不是很好 。
- 从不同的翻译单元实例化时,编译不会失败。
可以说,第一点只是不便。我倾向于不同意。想象一下,你已经重构了一些广泛使用的函数。这可能是对许多代码行的更改,无法编译 的子部分。因此,经过几天的工作,您第一次按“构建”,20 分钟后您会收到一条关于仅在返回类型上不同的函数的神秘消息。诚然,大多数编译器都给出了不错的跟踪,以便对所发生的事情的调查不会花费太长时间。然而,调查一开始就不应该是必要的。
第二点肯定更令人担忧。ASSERT_SINGLE_INSTANTIATION 不仅没有断言它承诺要断言的内容,而且由此产生的程序格式不正确,不需要诊断 。
有状态元编程
模板元编程是 C++ 中的一种技术,用于利用类型系统生成代码。在之前的几篇博文中(例如“ 约束用户定义的转换” 和“ 范围适配器的编译时大小 ”),我们利用它将计算从运行时转移到编译时。
直到几周前,我还生活在一个幸福的假设中,即 C++ 模板元编程纯粹是函数式编程 。哦,天哪,我错了吗?在纯函数式编程语言中,所有函数都是无副作用的。对于模板,这是正确的,只是大多数时候。通过一些神奇的朋友注射,我们可以跟踪和改变状态。令人惊讶的是,已经有许多关于有状态元编程的帖子被写了,例如“ 重新审视 C++20 中的有状态元编程 ”和“ 如何使用模板和朋友破解 C++”。
我们之前的解决方案暗示了有状态元编程。现在让我们充分利用它来修复神秘的错误消息。在我们的例子中,我们需要跟踪的状态是之前是否实例化了某些东西。本质可以归结为以下代码。这里,tc::string_template_param 是一个编译时字符串。复制
template<tc::string_template_param strFile, int nLine>
struct InstantiationLocation final {
friend auto InstantiatedFlag(InstantiationLocation) noexcept; // (1)
};
template<typename Location>
struct SetInstantiation final { // (2)
friend auto InstantiatedFlag(Location) noexcept {}
};
template<typename Location, auto UniqueTag>
[[nodiscard]] consteval auto Instantiate() noexcept { // (3)
if constexpr(requires { InstantiatedFlag(Location()); }) {
return false;
} else {
SetInstantiation<Location>();
return true;
}
}
在 (1) 处,我们声明(而不是定义)一个具有自动返回类型的函数。只要编译器不知道定义,就无法调用此函数。只有当我们实例化 (2) 时,才会注入定义。
现在,我们可以使用调用 (1) 的能力来指示我们是否已经实例化了某些东西。此检查发生在 (3) 中,如果我们已经可以调用 InstantiatedFlag,则返回 false 以指示实例化已经发生。否则,我们为 (1) 注入一个定义。
此实用程序可以与单行一起使用:复制
static_assert(Instantiate<InstantiationLocation<__FILE__, __LINE__>, []{}>(), "Should only be instantiated once!");
多个翻译单元怎么办
当使用多个翻译单元时,故事会更加复杂。由于编译器仅在单个转换单元上工作,因此由链接器来验证实例化是否只发生过一次。不幸的是,我们没有找到让链接器抛出错误的可靠方法;C++ 标准包含我们能想到的所有错误的“无需诊断”。
确保单个实例化的唯一剩余方法是在运行时。理想情况下,程序应尽早检测到,并因此可能失败。等到调用两个实例都不是健壮的,并且可能会产生性能损失。使用全局常量初始值设定项,我们可以进行所有检查,甚至在输入 main 之前。复制
template<typename Location>
void AssertSingleInstantiation() noexcept {
static constinit bool bIsInstantiated = false;
assert(!bIsInstantiated); // multiple instantiations across compilation units
bIsInstantiated = true;
}
template<typename Location, auto UniqueTag>
inline const auto c_AssertSingleInstantiationBeforeMain = []() noexcept {
AssertSingleInstantiation<Location>();
return 0;
}();
将之前的 static_assert 和实例 c_AssertSingleInstantiationBeforeMain 化相结合,会产生以下宏:复制
#define _ASSERT_SINGLE_INSTANTIATION { \
using Location = InstantiationLocation<__FILE__, __LINE__>; \
static_cast<void>(c_AssertSingleInstantiationBeforeMain<Location, []{}>); \
static_assert(Instantiate<Location, []{}>(), "Should only be instantiated once!"); \
}
您可以在编译器资源管理器中自己尝试一下!
在运行时检查(至少原则上)应该在编译期间检查的属性并不好。但是,在 main 之前运行这些检查是下一个最好的选择。如果您能找到一种方法来可靠地让链接器抛出错误,请告诉我们!
奖金
在撰写 MSVC 时,存在一个带有静态变量的代码生成错误 。使用 lambda(而不是静态变量的地址)作为唯一标签可以避免此错误。
您好,这是一条评论。若需要审核、编辑或删除评论,请访问仪表盘的评论界面。评论者头像来自 Gravatar。