在Visual Studio中,您可以为单个cpp文件设置不同的编译器选项。例如:在“代码生成”下,我们可以在调试模式下启用基本的运行时检查。或者我们可以更改浮点模型(精确/严格/快速)。这些只是示例。有很多不同的标志。
一个内联函数可以在程序中定义多次,只要定义相同即可。我们将此函数放在标题中,并将其包含在多个翻译单元中。现在,如果不同cpp文件中的不同编译器选项导致该函数的编译代码略有不同,会发生什么情况?那么它们确实有所不同,我们的行为不确定吗?您可以将函数设为静态(或将其置于未命名的命名空间中),但更进一步,直接在类中定义的每个成员函数都是隐式内联的。这意味着,如果这些cpp文件共享相同的编译器标志,则只能在不同的cpp文件中包含类。我无法想象这是真的,因为这基本上很容易出错。
在不确定的行为领域,我们真的那么快吗?还是编译器会处理这种情况?
答案 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约束的任何情况下,点类型似乎都是有问题的。