为什么链接器不保留封装?

Why doesn't linker preserve encapsulation?

让我们假设以下 header foo.h:

class Foo {
  private:
    void print() const;
};

和以下 foo.cpp:

#include <iostream>

#include "foo.h"

void Foo::print() const {
    std::cout << "Secret" << std::endl;
}

另一个 header foo1.h,与 foo.h 相同,除非方法 print 被声明 public:

class Foo {
  public:
    void print() const;
};

这将是 main.cpp,只需在 foo1.h 中调用 print:

#include "foo1.h"

int main() {
    Foo f;
    f.print();
    return 0;
}

对我来说奇怪的是以下链接会起作用:

g++ foo.cpp -c -o foo.o
g++ main.cpp -c -o main.o
g++ main.o foo.o -o exec
./exec

最后一条命令将输出:

Secret

所以不知道 class Foo 的具体实现,但知道它的声明并有它的 object 文件,我们可以创建可以使用它的方法的情况,即使它们被声明为私有。

我的问题是:

  1. 为什么有效?链接器不考虑私有和 public 声明?

  2. 这种行为在实践中有用吗?如果是,它是如何使用的?我猜它可能对测试有用。

链接器只解析符号。每个 C++ 文件都是独立编译的,无论声明是通过 #include 语句引入的,以构建符号(在 .o 文件中),然后链接器只处理编译器输出。 C++ 访问修饰符,如 private、protected 和 public 只影响编译器,不影响链接器。 (从技术上讲,链接器甚至不知道 classes,它只是处理 class 成员的 decorated/mangled 符号名称。)虽然不推荐,但可以修改 classes 更改成员的访问权限,实际上 "un-hide" 他们。

标准,第 [basic.def.odr] 节:

  1. There can be more than one definition of a class type [snip] in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. Given such an entity named D defined in more than one translation unit, then

    — each definition of D shall consist of the same sequence of tokens; and

您的程序违反了此规则,因为 class 的两个定义不包含相同的标记序列。违反单一定义规则会使程序格式错误。

  1. Why does it work?

格式不正确,标准未指定应如何处理这种情况。工具链可以自由拒绝link程序,但允许link成功。您的 linker 恰好是后者。另一个 link 人可能会做前者。

Linker doesn't consider private and public declarations?

正如您通过实验观察到的,您的 linker 似乎没有考虑访问说明符。 linker 不需要考虑它们。它们纯粹是一个编译时概念。

Is this behavior useful in practice? If yes, how is it used?

依赖于此 evil 且不可移植。也就是说,当源代码不可用于重新编译时,它有时会在野外被视为绕过 API 限制的肮脏黑客。

首先,由于您违反了 "One Definition Rule"(C++11 3.2/5 "One definition rule" 表示不同翻译单元中的单独 class 定义必须 "consist of the same sequence of tokens"),就工具链而言,任何事情都可以。它可以诊断错误,或生成一个似乎有效的程序(如在您的测试中)。

你的实验产生你看到的结果的一个简单原因是编译器对 class 成员的访问是 'enforced',并且你已经告诉编译器访问成员 Foo::print() 是 public。

工具链对因其他原因(例如重载)执行的名称 mangle 中的成员访问进行编码是符合要求的。但是,由于该标准不要求工具链强制执行它,因此实施者似乎决定他们不需要在 link 时考虑访问控制。换句话说,我认为将访问控制编码到 linker 使用的外部符号中是可行的,但这项工作尚未完成;可能是因为严格来说没有必要。

请注意,Microsoft C++ 确实在外部名称中合并了对成员的访问权限,因此您确实会遇到 link 时间错误:

testmain.obj : error LNK2019: unresolved external symbol "public: void __thiscall Foo::print(void)const " (?print@Foo@@QBEXXZ) referenced in function _main testmain.exe : fatal error LNK1120: 1 unresolved externals

这是 g++ 生成的符号(以及 c++filt 解码):

D:\so-test>nm test.o | grep Foo
000000000000008c t _GLOBAL__sub_I__ZNK3Foo5printEv
0000000000000000 T _ZNK3Foo5printEv

D:\so-test>nm testmain.o | grep Foo
                 U _ZNK3Foo5printEv

D:\so-test>c++filt _ZNK3Foo5printEv
Foo::print() const

这里是 MS C++ 生成的符号(连同解码):

D:\so-test>dumpbin /symbols test.obj | grep Foo
22D 00000000 SECTBA notype ()    External     | ?print@Foo@@ABEXXZ (private: void __thiscall Foo::print(void)const )

D:\so-test>dumpbin /symbols testmain.obj | grep Foo
009 00000000 UNDEF  notype ()    External     | ?print@Foo@@QBEXXZ (public: void __thiscall Foo::print(void)const )