我确实理解ODR所说的内容,但我不明白它想要实现的目标。
我看到违反它的两个后果 - 用户会得到语法错误,这完全没问题。并且可能存在一些致命的错误,并且用户将是唯一一个有罪的人。
作为违反ODR并遇到致命错误的例子,我想像这样:
struct A
{
int a;
double b;
};
void f(A a)
{
std::cout << a.a << " " << a.b << std::endl;
}
struct A
{
int a;
int b;
};
void f(A a);
int main()
{
A a = {5, 6};
f(a);
return 0;
}
如果示例与ODR无关,请纠正我。
那么,ODR是否试图禁止用户做这些有害的事情?我不这么认为。
是否试图为编译器编写者设置一些规则,以避免潜在的危害?可能不会,因为大多数编译器都没有检查ODR违规。
还有什么?
答案 0 :(得分:5)
当函数期望得到这些结构中的一个时,你将它重新声明为不同的东西,该函数接收哪个结构,以及如何?请记住,C ++是静态的,因此如果按值发送结构,则函数必须知道它的结构。因为C ++是类型安全的,允许违反ODR会违反这种类型的安全性。
最重要的是,缺乏ODR会有什么好处?我可以想到数以百计的事情,如果没有它就会变得更难,也无法获得。从能够在同一名称空间中踩踏先前声明的类型实际上没有灵活性。在最好的情况下,它只会使多个包含不需要标题保护,这是一个非常小的增益。
答案 1 :(得分:5)
ODR决定了C ++程序的良好形成。 ODR违规意味着您的程序格式不正确,并且标准没有规定程序将执行什么操作,是否应该编译等。大多数ODR违规都标记为&#34;无需诊断&#34;使编译器编写器的工作更容易。
这允许C ++编译器对你提供的代码做出某些简化的假设,比如::A
在任何地方都是相同的结构类型,而不必在每个使用点检查。
编译器可以自由地获取代码并将其编译为c:格式。或其他任何东西。可以自由检测ODR违规,并使用它来证明代码分支无法运行,并消除了导致那里的路径。
答案 2 :(得分:0)
据我所知,该规则的目的是防止在不同的翻译单元中对对象进行不同的定义。
// a.cpp
#include <iostream>
class SharedClass {
int a, b, c;
bool d;
int e, f, g;
public:
// ...
};
void a(const SharedClass& sc) {
std::cout << "sc.a: " << sc.getA() << '\n'
<< "sc.e: " << sc.getE() << '\n'
<< "sc.c: " << sc.getC() << std::endl;
}
// -----
// b.cpp
class SharedClass {
int b, e, g, a;
bool d;
int c, f;
public:
// ...
};
void b(SharedClass& sc) {
sc.setA(sc.getA() - 13);
sc.setG(sc.getG() * 2);
sc.setD(true);
}
// -----
// main.cpp
int main() {
SharedClass sc;
/* Assume that the compiler doesn't get confused & have a heart attack,
* and uses the definition in "a.cpp".
* Assume that by the definition in "a.cpp", this instance has:
* a = 3
* b = 5
* c = 1
* d = false
* e = 42
* f = -129
* g = 8
*/
// ...
a(sc); // Outputs sc.a, sc.e, and sc.c.
b(sc); // Supposedly modifies sc.a, sc.g, and sc.d.
a(sc); // Does NOT do what you think it does.
}
考虑到此计划,您可能会认为SharedClass
在a.cpp
和b.cpp
中的行为相同,因为它具有相同名称的相同字段。但请注意,字段的顺序不同。因此,每个翻译单元都会看到它(假设4字节整数和4字节对齐):
如果编译器使用隐藏的对齐成员:
// a.cpp
Class layout:
0x00: int {a}
0x04: int {b}
0x08: int {c}
0x0C: bool {d}
0x0D: [alignment member, 3 bytes]
0x10: int {e}
0x14: int {f}
0x18: int {g}
Size: 28 bytes.
// b.cpp
Class layout:
0x00: int {b}
0x04: int {e}
0x08: int {g}
0x0C: int {a}
0x10: bool {d}
0x11: [alignment member, 3 bytes]
0x14: int {c}
0x18: int {f}
Size: 28 bytes.
// main.cpp
One of the above, up to the compiler.
Alternatively, may be seen as undefined.
如果编译器将相同大小的字段放在一起,则从最大到最小排序:
// a.cpp
Class layout:
0x00: int {a}
0x04: int {b}
0x08: int {c}
0x0C: int {e}
0x10: int {f}
0x14: int {g}
0x18: bool {d}
Size: 25 bytes.
// b.cpp
Class layout:
0x00: int {b}
0x04: int {e}
0x08: int {g}
0x0C: int {a}
0x10: int {c}
0x14: int {f}
0x18: bool {d}
Size: 25 bytes.
// main.cpp
One of the above, up to the compiler.
Alternatively, may be seen as undefined.
请注意,如果您愿意,虽然两个定义中的类具有相同的大小,但其成员的顺序完全不同。
Field comparison (with alignment member):
a.cpp field b.cpp field
a b
b e
c g
d & {align} a
e d & {align}
f c
g f
Field comparison (with hidden reordering):
a.cpp field b.cpp field
a b
b e
c g
e a
f c
g f
d d
因此,从a()
的角度来看,b()
实际上更改了sc.e
,sc.c
以及sc.a
或sc.d
(取决于它是如何编译的),完全改变第二个呼叫的输出。 [请注意,这甚至可能出现在您无法预料到的无谓情况下,例如a.cpp
和b.cpp
对SharedClass
的定义相同,但指定不同的对齐方式。这将改变对齐成员的大小,再次为不同的翻译单元提供不同的内存布局。]
现在,如果相同的字段在不同的翻译单元中的布局不同,会发生什么。想象一下,如果该类在不同单元中具有完全不同的字段,会发生什么。
// c.cpp
#include <string>
#include <utility>
// Assume alignment of 4.
// Assume std::string stores a pointer to string memory, size_t (as long long), and pointer
// to allocator in its body, and is thus 16 (on 32-bit) or 24 (on 64-bit) bytes.
// (Note that this is likely not the ACTUAL size of std::string, but I'm just using it for an
// example.)
class SharedClass {
char c;
std::string str;
short s;
unsigned long long ull;
float f;
public:
// ...
};
void c(SharedClass& sc, std::string str) {
sc.setStr(std::move(str));
}
在此文件中,我们的SharedClass
将是这样的:
Class layout (32-bit, alignment member):
0x00: char {c}
0x01: [alignment member, 3 bytes]
0x04: string {str}
0x14: short {s}
0x16: [alignment member, 2 bytes]
0x18: unsigned long long {ull}
0x20: float {f}
Size: 36 bytes.
Class layout (64-bit, alignment member):
0x00: char {c}
0x01: [alignment member, 3 bytes]
0x04: string {str}
0x1C: short {s}
0x1E: [alignment member, 2 bytes]
0x20: unsigned long long {ull}
0x28: float {f}
Size: 44 bytes.
Class layout (32-bit, reordered):
0x00: string {str}
0x10: unsigned long long {ull}
0x18: float {f}
0x1C: short {s}
0x1E: char {c}
Size: 31 bytes.
Class layout (64-bit, reordered):
0x00: string {str}
0x18: unsigned long long {ull}
0x20: float {f}
0x24: short {s}
0x26: char {c}
Size: 39 bytes.
此 SharedClass
不仅有不同的字段,而且大小完全不同。尝试将每个翻译单元视为具有相同的SharedClass
,并且将打破某些内容,并且无法默认地协调每个定义。想象一下,如果我们在a()
的同一个实例上调用b()
,c()
和SharedClass
,或者如果我们尝试 make SharedClass
的实例。有三种不同的定义,并且编译器不知道哪一个是实际的定义,事情可以和将变坏。
这完全打破了单元间的可操作性,要求所有使用类的代码都在同一个转换单元中,或者在每个单元中共享完全相同的类定义。因此,ODR要求每个单元只定义一次类,并在所有单元中共享相同的定义,以保证它始终具有相同的定义,并防止这整个问题。
同样,请考虑这个简单的函数func()
。
// z.cpp
#include <cmath>
int func(int x, int y) {
return static_cast<int>(round(pow((2 * x) - (3 * y), x + y) - (x / y)));
}
// -----
// y.cpp
int func(int x, int y) { return x + y; }
// -----
// x.cpp
int q = func(9, 11);
// Compiler has a heart attack, call 911.
编译器无法确定您所指的func()
的哪个版本,并且实际上将它们视为相同的功能。这自然会破坏事物。当一个版本具有副作用(例如改变全局状态或导致内存泄漏),而另一个版本没有副作用时,情况会变得更糟。
在这种情况下,ODR旨在保证任何给定的功能将在所有翻译单元中共享相同的定义,而不是在不同的单元中具有不同的定义。这个更容易改变(通过将所有函数视为inline
用于ODR的目的,但是如果显式或隐式声明为inline
,则仅将其视为// i.cpp
int global_int;
namespace Globals {
int ns_int = -5;
}
// -----
// j.cpp
int global_int;
namespace Globals {
int ns_int = 5;
}
),但这可能导致不可预知的麻烦。
现在,考虑一个更简单的案例,即全局变量。
global_int
在这种情况下,每个翻译单元定义变量Globals::ns_int
和Globals::ns_int
,这意味着程序将具有两个不同的变量,具有完全相同的错位名称。这只能在链接阶段很好地结束,其中链接器将符号的每个实例视为引用相同的实体。由于将两个不同的初始化值硬编码到文件中,global_int
将出现比{{1}}更多的问题;假设链接器不会爆炸,程序将保证具有未定义的行为。
ODR的复杂程度各不相同,具体取决于相关实体。有些东西在整个程序中只能有一个定义,但有些可以有多个定义,只要它们完全相同并且每个翻译单元只有一个定义。无论如何,意图是每个单位都会以完全相同的方式看待实体。
但主要原因是方便。编译器不仅更容易假设每个翻译单元都遵循ODR,它更快,更少CPU,内存和磁盘密集。如果没有ODR,编译器必须比较每个转换单元,以确保每个共享类型和内联函数定义相同,并且每个全局变量和非内联函数仅在单个转换单元中定义。当然,这需要它在编译任何单元时从磁盘加载每个单元,使用许多系统资源,如果程序员遵循良好的编程习惯,它实际上需要它。鉴于此,迫使程序员遵循ODR让编译器认为一切都很好,花花公子,使其工作(以及程序员在等待编译器时工作和/或完成工作)更容易。 [与此相比,确保在一个单元内遵循ODR是孩子的游戏。]
答案 3 :(得分:0)
简单地说,“一个定义规则”保证:
在程序中应该只定义一次的实体只定义了一次。
可以在多个转换单元(类,内联函数,模板函数)中定义的实体具有等同的定义,从而产生等同的编译代码。等价必须完美,才能在运行时使用任何一个定义:许多定义是无法区分的。