内联函数在不同的翻译单元中具有不同的编译器标志未定义的行为?

时间:2018-08-28 05:48:52

标签: c++ language-lawyer one-definition-rule inline-functions translation-unit

在Visual Studio中,您可以为单个cpp文件设置不同的编译器选项。例如:在“代码生成”下,我们可以在调试模式下启用基本的运行时检查。或者我们可以更改浮点模型(精确/严格/快速)。这些只是示例。有很多不同的标志。

一个内联函数可以在程序中定义多次,只要定义相同即可。我们将此函数放在标题中,并将其包含在多个翻译单元中。现在,如果不同cpp文件中的不同编译器选项导致该函数的编译代码略有不同,会发生什么情况?那么它们确实有所不同,我们的行为不确定吗?您可以将函数设为静态(或将其置于未命名的命名空间中),但更进一步,直接在类中定义的每个成员函数都是隐式内联的。这意味着,如果这些cpp文件共享相同的编译器标志,则只能在不同的cpp文件中包含类。我无法想象这是真的,因为这基本上很容易出错。

在不确定的行为领域,我们真的那么快吗?还是编译器会处理这种情况?

3 个答案:

答案 0 :(得分:2)

就标准而言,命令行标志的每种组合都将编译器转变为不同的实现。尽管能够使用其他实现产生的目标文件对实现很有用,但该标准并不强制要求这样做。

即使没有内联,也应考虑在一个编译单元中具有以下功能:

char foo(void) { return 255; }

以及以下内容:

char foo(void);
int arr[128];
void bar(void)
{
  int x=foo();
  if (x >= 0 && x < 128)
     arr[x]=1;
}

如果两个编译单元中的char是带符号的类型,则第二个单元中的x的值将小于零(从而跳过数组分配)。如果在两个单位中均为无符号类型,则该值将大于127(同样跳过分配)。但是,如果一个编译单元使用有符号的char,而另一个使用无符号的x,并且如果实现期望结果寄存器中的返回值进行符号扩展或零扩展,则结果可能是编译器可以确定arr[255]即使拥有255也不能大于127,或者即使拥有-1也不能小于0。因此,生成的代码可能会访问arr[-1]Provider=Microsoft.Jet.OLEDB.4.0;Data Source="C:\SomePath\MyDatabase.mdb";,结果可能会造成灾难性的后果。

尽管在许多情况下使用不同的编译器标记来组合代码应该是安全的,但标准并未做出任何区分安全混合和不安全混合的区分。

答案 1 :(得分:1)

如果这个问题确实存在,我最近为GCC测试编写了一些代码。

SPOILER:是的。

设置:

我正在使用AVX512指令编译一些代码。由于大多数cpus不支持AVX512,因此我们需要在没有AVX512的情况下编译大部分代码。问题是:用AVX512编译的cpp文件中使用的内联函数是否可以用非法指令“毒化”整个库。

想象一下一种情况,非AVX512 cpp文件中的函数调用了我们的函数,但它命中了来自AVX512编译单元的程序集。这将使我们在非AVX512机器上illegal instruction

让我们尝试一下:

func.h

inline void __attribute__ ((noinline)) double_it(float* f) {
  for (int i = 0; i < 16; i++)
    f[i] = f[i] + f[i];
}

我们定义了一个内联(在链接器意义上)函数。使用硬编码16将使GCC优化器使用AVX512指令。我们必须使其成为((noinline))以防止编译器对其进行内联(即将其代码粘贴到调用方)。这是一种廉价的方法,可以假装此功能太长而无法内联。

avx512.cpp

#include "func.h"
#include <iostream>

void run_avx512() {
  volatile float f = 1;
  float arr [16] = {f};
  double_it(arr);
  for (int i = 0; i < 16; i++)
    std::cout << arr[i] << " ";
  std::cout << std::endl;
}

这是AVX512对我们的double_it函数的使用。它将一些数组加倍并打印结果。我们将使用AVX512进行编译。

non512.cpp

#include "func.h"
#include <iostream>

void run_non_avx() {
  volatile float f = 1;
  float arr [16] = {f};
  double_it(arr);
  for (int i = 0; i < 16; i++)
    std::cout << arr[i] << " ";
  std::cout << std::endl;
}

与以前的逻辑相同。这个不会用AVX512编译。

lib_user.cpp

void run_non_avx();

int main() {
  run_non_avx();
}

一些用户代码。调用在没有AVX512的情况下编译的`run_non_avx。它不知道会不会冒出来:)

现在,我们可以编译这些文件并将它们链接为共享库(也许常规lib也可以工作)

g++ -c avx512.cpp -o avx512.o -O3 -mavx512f -g3 -fPIC
g++ -c non512.cpp -o non512.o -O3 -g3 -fPIC
g++ -shared avx512.o non512.o -o libbad.so
g++ lib_user.cpp -L . -lbad -o lib_user.x
./lib_user.x

在我的计算机上运行此文件(没有AVX512)会给我

$ ./lib_user.x
Illegal instruction (core dumped)

另一方面,如果我更改avx512.o non512.o的顺序,它将开始工作。链接器似乎忽略了相同功能的后续实现。

答案 2 :(得分:0)

  

内联函数可以在程序中多次定义,如下   只要定义相同

不。 (“完全相同”在这里甚至不是一个定义明确的概念。)

从某种意义上讲,正式的定义必须是等效的,这甚至没有必要,而且没人在乎:

// in some header (included in multiple TU):

const int limit_max = 200; // implicitly static

inline bool check_limit(int i) {
  return i<=limit_max; // OK
}

inline int impose_limit(int i) {
  return std::min(i, limit_max); // ODR violation
}

这样的代码是完全合理的,但是正式违反了一个定义规则:

  在D的每个定义中,

对应的名称根据   6.4 [basic.lookup],应引用D定义中定义的实体,或应在重载后引用相同的实体   分辨率(16.3 [over.match])和部分模板匹配后   专业化(17.9.3 [temp.over]),但名称可以引用   具有内部链接或没有链接的const对象(如果对象具有相同的链接)   D的所有定义中的文字类型,并且对象已初始化   带有常量表达式(8.20 [expr.const]),和值(但不是)   地址),并且该对象具有相同的值   在D的所有定义中;

因为该异常不允许将具有内部链接的const对象(const int隐式静态)用于直接绑定const引用(然后仅将该引用用于其值)。正确的版本是:

inline int impose_limit(int i) {
  return std::min(i, +limit_max); // OK
}

此处limit_max的值用于一元运算符+,并且然后将const引用绑定到使用该值初始化的临时值。谁真正做到了?

但正如我们在Core Issue 1511中所看到的,即使委员会也不认为正式的ODR很重要:

  

1511。 const易变变量和一定义规则

     

部分:6.2 [basic.def.odr]状态:CD3提交者:理查德   史密斯日期:2012-06-18

     

[在2013年4月的会议上移至DR。]

     

对于这样的示例,此措词可能不够清楚

  const volatile int n = 0;
  inline int get() { return n; }

我们看到,委员会认为,这种公然违反了书面ODR的意图和目的,该代码在每个TU中读取不同的易失性对象,即该代码具有不同对象的可见副作用,所以不同可见副作用是可以的,因为我们不关心 哪个是< / strong>。

重要的是,内联函数的效果几乎是等效的:执行易失的int读取,这是一个非常弱的等效项,但足以满足ODR的自然使用的要求, >实例无关紧要:内联函数使用哪个特定实例无关紧要,也无济于事

特别是,根据定义,易失性读取所读取的值是编译器不知道的,因此编译器分析的该函数的发布条件和不变量是相同的。

在不同的TU中使用不同的函数定义时,您需要确保从调用者的角度来看它们是严格等效的:永远不可能通过用一个替代另一个来使调用者感到惊讶。这意味着即使代码不同,可观察的行为也必须严格相同

如果您使用不同的编译器选项,则它们不得更改函数可能结果的范围(可能由编译器查看)。

因为“标准”(实际上不是编程语言的规范)允许浮点对象使用任何非易失性合格的浮点,以完全不受限制的方式具有其正式声明的类型所不允许的真实表示。除非您激活“ double意思是double”模式(这是唯一的理智模式),否则在受ODR约束的任何情况下,点类型似乎都是有问题的。