ODR的目的是什么?

时间:2016-06-25 16:52:15

标签: c++ language-design linkage one-definition-rule

我确实理解ODR所说的内容,但我不明白它想要实现的目标。

我看到违反它的两个后果 - 用户会得到语法错误,这完全没问题。并且可能存在一些致命的错误,并且用户将是唯一一个有罪的人。

作为违反ODR并遇到致命错误的例子,我想像这样:

a.cpp

struct A
{
        int a;
        double b;
};
void f(A a)
{
        std::cout << a.a << " " << a.b << std::endl;
}

的main.cpp

struct A
{
        int a;
        int b;

};
void f(A a);

int main()
{

        A a = {5, 6};
        f(a);

        return 0;
}

如果示例与ODR无关,请纠正我。

那么,ODR是否试图禁止用户做这些有害的事情?我不这么认为。

是否试图为编译器编写者设置一些规则,以避免潜在的危害?可能不会,因为大多数编译器都没有检查ODR违规。

还有什么?

4 个答案:

答案 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.
}

考虑到此计划,您可能会认为SharedClassa.cppb.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.esc.c以及sc.asc.d (取决于它是如何编译的),完全改变第二个呼叫的输出。 [请注意,这甚至可能出现在您无法预料到的无谓情况下,例如a.cppb.cppSharedClass的定义相同,但指定不同的对齐方式。这将改变对齐成员的大小,再次为不同的翻译单元提供不同的内存布局。]

现在,如果相同的字段在不同的翻译单元中的布局不同,会发生什么。想象一下,如果该类在不同单元中具有完全不同的字段,会发生什么。

// 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_intGlobals::ns_int,这意味着程序将具有两个不同的变量,具有完全相同的错位名称。这只能在链接阶段很好地结束,其中链接器将符号的每个实例视为引用相同的实体。由于将两个不同的初始化值硬编码到文件中,global_int将出现比{{1}}更多的问题;假设链接器不会爆炸,程序将保证具有未定义的行为。

ODR的复杂程度各不相同,具体取决于相关实体。有些东西在整个程序中只能有一个定义,但有些可以有多个定义,只要它们完全相同并且每个翻译单元只有一个定义。无论如何,意图是每个单位都会以完全相同的方式看待实体。

但主要原因是方便。编译器不仅更容易假设每个翻译单元都遵循ODR,它更快,更少CPU,内存和磁盘密集。如果没有ODR,编译器必须比较每个转换单元,以确保每个共享类型和内联函数定义相同,并且每个全局变量和非内联函数仅在单个转换单元中定义。当然,这需要它在编译任何单元时从磁盘加载每个单元,使用许多系统资源,如果程序员遵循良好的编程习惯,它实际上需要它。鉴于此,迫使程序员遵循ODR让编译器认为一切都很好,花花公子,使其工作(以及程序员在等待编译器时工作和/或完成工作)更容易。 [与此相比,确保在一个单元内遵循ODR是孩子的游戏。]

答案 3 :(得分:0)

简单地说,“一个定义规则”保证:

  1. 在程序中应该只定义一次的实体只定义了一次。

  2. 可以在多个转换单元(类,内联函数,模板函数)中定义的实体具有等同的定义,从而产生等同的编译代码。等价必须完美,才能在运行时使用任何一个定义:许多定义是无法区分的