仅在标头中的外部变量意外工作,原因是什么



我目前正在更新Arduino的C++库(特别是使用AVR gcc编译的8位AVR处理器)。

通常,默认Arduino库的作者喜欢在头中为类包含一个外部变量,该变量也在class.cpp文件中定义。我认为这基本上是为了让新手可以随时使用所提供的一切作为内置对象。

我的场景是:我更新的库不再需要.cpp文件,我已经将其从库中删除。直到最后一次检查错误时,我才意识到,尽管.cpp文件中没有为extern变量提供定义,但没有产生链接器错误。

这是我能得到的最简单的(头文件):

struct Foo{
  void method() {}
};
extern Foo foo;

包含此代码并在一个或多个源文件中使用它不会导致任何链接器错误。我在Arduino使用的GCC的两个版本(4.3.7、4.8.1)和启用/禁用C++11的情况下都尝试过它。

在我试图造成错误的过程中,我发现只有在执行诸如获取对象地址或修改我添加的伪变量的内容之类的操作时,才有可能出现错误。

在发现这一点后,我发现它很重要:

  • 类函数只返回其他对象,如中所示,与运算符返回对自身的引用,甚至返回副本完全不同
  • 它只修改外部对象(实际上是代码中volatile uint8_t引用的寄存器),并返回其他类的临时值
  • 这个头中的所有类函数都是非常基本的,它们的开销小于或等于函数调用的开销,因此它们(在我的测试中)完全与调用者对齐。一个典型的语句可能会在调用链中创建许多临时对象,但编译器会看穿这些对象,并直接输出有效的代码修改寄存器,而不是一组嵌套的函数调用

我还记得在n37977.1.1-8中读到extern可以用于不完整的类型,但是类是完全定义的,而声明不是(这可能无关紧要)。

我相信这可能是优化的结果。我已经看到了获取地址对对象的影响,否则这些对象将被视为常量并在不使用RAM的情况下编译。通过向编译器无法保证状态的对象添加任何间接层,将导致这种RAM消耗行为。

所以,也许我只是简单地问了一下就回答了我的问题,但我仍然在做假设,这让我很困扰。在对C++编程有了很长一段时间的爱好之后,我的不做列表中唯一的事情就是做假设

真的,我想知道的是:

  • 关于我的工作解决方案,这是一个记录无法获取类地址(导致间接寻址)的简单案例吗
  • 这只是一种由优化引起的边缘情况行为吗?优化消除了对链接内容的需求
  • 或者是简单明了的未定义行为。在GCC中可能有一个错误,并且允许在降低或禁用优化时可能失败的代码吗

或者,你们中的一个人可能足够幸运,拥有一个解码器环,可以在标准中找到一个合适的段落来概述细节。

这是我在这里的第一个问题,所以如果你想知道某些细节,请告诉我,如果需要,我也可以提供GitHub代码链接。

编辑:由于库需要与现有代码兼容,我需要保持使用点语法的能力,否则我只需要一类静态函数。

为了暂时消除假设,我看到了两个选项:

  • 仅为变量声明添加.cpp
  • 在标题中使用类似#define foo (Foo())的定义,允许通过临时的点语法

我更喜欢使用定义的方法,社区怎么想?

干杯。

声明extern只是通知汇编程序和链接器,无论何时使用该标签/符号,它都应该引用符号表中的条目,而不是本地分配的符号。

链接器的作用是尽可能用对地址空间的实际引用来替换符号表条目。

如果您在C文件中根本不使用该符号,它将不会显示在程序集代码中,因此当您的模块与其他模块链接时,也不会导致任何链接器错误,因为没有未定义的引用。

这要么是由优化引起的边缘情况行为,要么您从未在代码中使用foo变量。我不能100%确定它在形式上不是一种未定义的行为,但我很确定从实践的角度来看它不是未定义的。

extern变量是以这样的方式实现的,使用它们编译的代码会产生所谓的重定位——应该放置变量地址的空位置——然后由链接器填充。显然,foo在代码中从未以需要获取其地址的方式使用过,因此链接器甚至不会试图找到该符号。如果您关闭优化(-O0),您可能会得到链接器错误。

更新:如果你想保留"点表示法",但消除未定义外部的问题,你可以用static(在头文件中)替换extern,为每个TU创建单独的变量"实例"。由于这个变量无论如何都会被优化,这根本不会改变实际代码,但也适用于未优化的构建。

最新更新