为什么我们不直接使用 Object 文件呢?

Why don't we use directly Object files?

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

我正在阅读:

GCC Visibility

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

创建 static/shared 库时,我们丢失了很多信息,也失去了优化的机会(这对我来说无关紧要)。相反,我们可以将源文件编译成 Obj 文件,然后 link 所有 Obj 文件(也是库的 Obj 文件)成为最终的可执行文件。

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

如果可见性让您担心,您可以执行 "Ghost Compile/Link" 步骤(编译应用程序,然后 link 使用静态库查看我们是否得到 "undefined symbol" 因为访问内部内容)和然后继续真正的Compile/Link(所有东西到Obj文件,然后Obj到可执行文件)

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

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

小例子:

我的静态库是从 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

当我们link我们得到

MyLib.a

MyBar,仍然可以"see"调用MyFoo(否则无法编译)。

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

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

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

这是抛出异常或尝试使用 std::type_info

时的问题

所以唯一可行的解​​决方案似乎是进行两步编译,第一步只是为了检查我们没有破坏可见性(因此 API 合同),第二步是为了避免上述问题link(调试异常行为或莫名其妙的崩溃很奇怪)。

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

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

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

一个库方便打包。

  • 大量的代码通常会有很多目标文件。对于库的最终用户来说,管理与每个单独的目标文件的链接可能会造成不必要的混乱和不知所措。

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

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

用 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 是什么。您的评论之一:我将无法 dynamic_cast<Foo *>(ptr)。真的吗?如果预期 Foo 是私有的,那么为什么您的用户应该能够动态转换为它?!?如果你认为你可以给我任何一个理由,那就知道你完全错了。

Foo 也可以抛出私有异常:

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

你有两个主要的解决方案。

当你实现 Foo 时,你知道它本身是私有的,所以最简单的方法是永远不要引发私有异常(这通常没有多大意义,但你可能想要这样。 ..)

如果必须有私有异常,则必须有异常的转换。因此,在 Bar 中,您捕获所有来自 Foo 的私有异常并将它们转换为 public 异常,然后将其抛出。坦率地说,一方面,这需要做更多的工作,但最重要的是你要扔两次:慢!

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());
        }
    }
};

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

主题扩展

由于您看起来对库之间的大量信息丢失这一事实很感兴趣,因此您可能对 Ada 语言而不是 C++ 感兴趣。在 Ada 中,目标文件实际上包含了进一步编译其他包和可执行文件所需的所有信息,而不仅仅是进一步链接。由于 Ruby on Rail 是 Ada 和 Eiffel 的产物,你肯定会在该语言的目标文件中出现相同的效果,虽然我不熟悉它所以我不能说(虽然我不知道如何如果没有它,他们会成功的!)

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