为什么我们不直接使用对象文件?

Why don't we use directly Object files?

本文关键字:对象 文件 我们 为什么      更新时间:2023-10-16

^不是重复的,在这里我正在研究为什么一种方法是首选的,而不是差异,仔细阅读会让同行评审员看到它不是重复的。

我正在阅读:

海湾合作委员会可见性

突然,这个问题突然出现在我的脑海中。是否有任何理智的理由使静态或共享库优于普通 Obj 文件?

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

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

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

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

您是否看到可能的问题(我已经使用它并且工作完美无缺,但也许已经存在的代码可能存在"重复"问题?也许对于大型代码库不可行?

小例子:

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

MyFoo.hpp

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

嗡.cpp

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

我的酒吧.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 wich 是正确的

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

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

引发异常或尝试使用std::type_info时,这是一个问题

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

链接MyFoo.oMyBar.omain.o togheter在概念上是错误的,因为允许编译以下代码

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

但是链接对象文件切换是避免异常问题的唯一方法。

将事物隐藏在C++

你可能想更多地了解,作为C++程序员,你应该如何隐藏私有定义。主要选项之一是痘痘成语:

为什么要使用"PIMPL"成语?

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

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

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

Bar.hpp中,您不能引用Foo,如有必要,也可以使用其指针:

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

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

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

你也可以在 Bar 内部实现Foo.cpp假设它不是太大。这样你也可以使用no name命名空间(即隐藏声明而不使用g++技巧):

// in Bar.cpp
namespace
{
Class Foo
{
    ...implementation of Foo...
};
} // no name namespace
Class Bar
{
    ...implementation of Bar, can reference Foo...
};

规避信息丢失

现在

,您的主要观点是关于信息的丢失,因为现在您不知道Foo是什么。你的评论之一:我将无法dynamic_cast<Foo *>(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());
        }
    }
};

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

关于该主题的扩展

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

否则,C/C++ 对象文件和库已由其他人解释。我对他们自己的评论没有太多要补充的。请注意,这些自 60 年代(可能是 70 年代初)以来就存在,尽管格式发展了很多,但文件仍然像过去一样非常局限于text(即编译 + 汇编代码)块。

库是方便的打包。

  • 大型代码通常具有许多对象文件。 管理与每个单独对象文件的链接可能会给库的最终用户带来不必要的混乱和压倒性。

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

  • 与工具箱一样,在公共位置具有相关功能更容易。