【问题标题】:Why don't we use directly Object files? [duplicate]为什么我们不直接使用 Object 文件? [复制]
【发布时间】:2015-01-19 01:52:53
【问题描述】:

^ 不是重复的,在这里我正在调查为什么首选一种方法,而不是有什么区别,仔细阅读会使同行评审员感到震惊,这不是重复的。

我正在阅读:

GCC Visibility

我的脑海里突然冒出这个问题。是否有任何合理的理由使静态或共享库优先于普通的 Obj 文件?

在创建静态/共享库时,我们会丢失很多信息以及优化的机会(这对我来说并不重要)。相反,我们可以将源文件编译成 Obj 文件,然后将所有 Obj 文件(也是库的 Obj 文件)链接到最终的可执行文件中。

=> 我们保留所有信息,对于异常处理和防止重复的 type_info 尤其有用(C++ 的依赖注入依赖于 C++11 Type_info,但不能保证您不会为不同库中的不同类获得重复的 std::type_info 对象)。

如果可见性让您担心,您可以执行“Ghost 编译/链接”步骤(编译应用程序,然后与静态库链接以查看我们是否因为访问内部内容而获得“未定义符号”),然后继续进行真正的编译/Link(一切都到 Obj 文件,然后 Obj 到可执行文件)

最终的可执行文件将更小更快,不会出现异常问题,并且构建系统将更加 Emscripten 友好:)。

您是否发现可能存在的问题(我已经在使用它并且工作正常,但可能已经存在的代码可能存在“重复”问题?对于大型代码库可能不可行?)

小例子:

我的静态库是从 2 个文件编译而来的:

MyFoo.hpp

//declare MyFoo to be internal to my library
void __attribute__ ((visibility ("hidden"))) MyFoo();

MyFoo.cpp

#include <MyFoo.hpp>
#include <iostream>
void MyFoo(){
    std::cout<<"MyFoo()"<<std::endl;
}

MyBar.cpp

#include <MyBar.hpp>
#include <MyFoo.hpp>
#include <iostream>
void MyBar(){
    std::cout<<"MyBar() calling "; MyFoo(); //calling MyFoo
}

当我们编译时,我们得到 2 个 ObjFiles

MyFoo.o
MyBar.o

当我们链接时我们得到

MyLib.a

MyBar,仍然可以“看到”并调用 MyFoo(否则无法编译)。

当我创建可执行文件时,如果我链接到 MyLib.a,我只能调用 MyBar,这是正确的

#include <MyBar.hpp>
#include <MyFoo.hpp>
int main(){
    MyBar(); //ok
    //MyFoo(); // error undefined symbol
    return 0;
}

那是因为我丢失了一些信息(在大多数情况下需要:注意我必须指定 hidden),但是因此“可见性”功能变成了一个问题,因为隐藏东西我们最终可能会使用 不同的类(在不同的库中)具有相同的完整限定名:

这是抛出异常或尝试使用std::type_info时的问题

所以唯一可行的解​​决方案似乎是进行 2 步编译,1 只是为了检查我们没有破坏可见性(因此 API 合同),第二次构建是为了避免上述链接中的问题(调试异常很奇怪行为或神秘的崩溃)。

将MyFoo.o,MyBar.o,main.o 链接在一起在概念上是错误的,因为允许编译以下代码

#include <MyBar.hpp>
#include <MyFoo.hpp>
int main(){
    MyBar(); //ok
    MyFoo(); // compile NOT OK!
    return 0;
}

但是将目标文件链接在一起是避免异常问题的唯一方法。

【问题讨论】:

  • 建库时会丢失哪些信息?它们只是一堆与ar 连接的目标文件。
  • 当你玩的时候没有班级的可见性。您可以#include &lt;foo&gt; 并在创建库时使用 foo(因此 foo 在对象文件级别可见),但是当您创建可执行文件并链接到静态库时,您会得到 foo 的未定义符号(您丢失了有关 foo 的信息)。所以实际上隐藏信息通常是好的,如果你把它隐藏给最终用户是好的,但可能会成为异常处理和 dynamic_cast 的问题。
  • @DarioOO 我还是不明白,你能举个小例子说明你的意思吗?
  • 静态库本质上是目标文件的 ZIP 文件(尽管使用不同的格式,实际上不是 ZIP)。
  • @DarioOO:需要明确的是,#include 包括源文件,而不是目标文件。

标签: c++ c++11 gcc linker


【解决方案1】:

在 C++ 中隐藏东西

您可能想进一步了解作为 C++ 程序员,您应该如何隐藏私有定义。主要选项之一是 Pimpl Idiom:

Why should the "PIMPL" idiom be used?

这使得 Foo.hpp 包含完全私有的,甚至直接在 Bar.cpp 实现中部分定义(取决于它将有多大。)

# project organization
include/Bar.hpp
src/Bar.cpp
src/Foo.hpp
src/Foo.cpp

include 下的文件将与其他项目共享。 src 下的任何内容都是 100% 私有的(例如,如果您查看 Qt 源代码,您会看到 Private.h 文件是 pimpl。)

在Bar.hpp中你不能引用Foo,如果需要你也可以使用它的指针:

class Foo;

class Bar
{ 
   ...
   std::shared_ptr<Foo> f_foo;
   ...
};

然后在 .cpp 中包含 .hpp 以获得实际定义:

#include "Bar.hpp"
#include "Foo.hpp"
...
// Bar implementation
...

您也可以在 Bar.cpp 中实现Foo,前提是它不是太大。这样你也可以使用无名命名空间(即隐藏没有 g++ 技巧的声明):

// in Bar.cpp

namespace
{

Class Foo
{
    ...implementation of Foo...
};

} // no name namespace

Class Bar
{
    ...implementation of Bar, can reference Foo...
};

避免信息丢失

现在,您的主要观点是关于信息的丢失,因为现在您不知道 Foo 是什么。你的一个cmets:我不能dynamic_cast&lt;Foo *&gt;(ptr)。真的吗?如果 Foo 预计是私有的,那么为什么您的用户应该能够动态转换到它?!?如果你认为你可以给我任何一个理由,那就知道你完全错了。

Foo 也可以抛出一个私有异常:

class Foo
{
   ...
   void func() { throw FooException("ouch!"); }
   ...
};

对于这个问题,你有两个主要的解决方案。

当您实现 Foo 时,您知道它本身将是私有的,因此最简单的方法是永远不要引发私有异常(这通常没有多大意义,但您可能希望拥有这样的... )

如果你必须有私有异常,你必须有一个异常的转换。因此,在 Bar 中,您从 Foo 捕获所有私有异常并将它们转换为公共异常,然后将其抛出。坦率地说,这需要更多的工作,一方面,但除此之外,你还要扔两次:慢!

class Foo
{
   ...
   // Solution 1, use a public exception from the start
   void func() { throw BarException("ouch!"); }
   ...
};

// Solution 2, convert the exception
class Bar
{
    void func()
    {
        try
        {
           f_foo.func();
        }
        catch(FooException const& e)
        {
            throw BarException(e.what());
        }
    }
};

有时您别无选择,只能捕获异常并转换它们,因为子类不能直接抛出您的公共异常(可能是第 3 方库)。但坦率地说,如果可以的话,请使用公共异常。

主题扩展

由于您看起来对在库之间丢失大量信息这一事实感兴趣,因此您可能对 Ada 语言而不是 C++ 感兴趣。在 Ada 中,目标文件实际上包含了进一步编译其他包和可执行文件所需的所有信息,而不仅仅是进一步的链接。由于 Ruby on Rail 是 Ada 和 Eiffel 的衍生品,因此您肯定会在该语言对象文件中出现相同的效果,尽管我不熟悉它,所以我无法真正分辨(尽管我不知道它们是如何实现的没有这样的工作!)

否则,C/C++ 目标文件和库已被其他人解释。我没有太多可以添加到他们自己的 cmets 中。请注意,这些从 60 年代(可能是 70 年代初)就存在了,尽管格式发生了很大变化,但文件仍然非常受限于 text(即编译 + 汇编代码)块,就像过去一样。

【讨论】:

  • 如果你认为你没有抓住重点,关于可见性的例子是应评论者的要求,我永远不会做这样的设计决定(用“预处理器”隐藏 API,相反我已经有 src 文件夹.. )没有理智的人会提出私人例外,但如果有人因为需要它而开始使用可见性(当你“需要”设置可见性时,链接中解释了)它很可能会覆盖全局可见性设置,这会使例外意外成为私人尽管有良好的意图。关键在于直接使用 Obj(这会间接忽略可见性)
【解决方案2】:

库是方便的包装。

  • 大量代码通常包含许多目标文件。管理与每个单独对象文件的链接可能会给库的最终用户带来不必要的困惑和压力。

  • 目标文件可能会在不同版本之间更改名称,即使界面相同,并且对最终用户而言没有差异。将这些更改保留在库抽象中可以让最终用户的生活变得更简单。

  • 就像一个工具箱,在一个共同的地方更容易拥有相关的功能。

【讨论】:

  • 所以我可以将所有内容链接到一个 Library.o ,它是一个目标文件(更好的是,从许多源文件直接编译为单个 obj)但仍然不是通常意义上的库,我具备您上面提到的所有 3 个优点。
  • 虽然使用静态库链接器链接可以省略不需要的目标文件(没有人使用的目标文件),这可能会导致较小的可执行文件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-16
相关资源
最近更新 更多