c++编译设计:安全地扩展类

C++ Compilation Design: Safely extending a class

本文关键字:扩展 安全 编译 c++      更新时间:2023-10-16

我有一个关于扩展已经使用的头,源和对象的问题。在理解我的意思之前,你必须接受我想使用这种设计:

在我的一个项目中,我只在头文件中使用函数声明,对于每个定义我使用一个单独的源文件,它将编译成一个单独的目标文件。

假设我在目录"src"中有一个非常简单的类List。

标题可以是:

文件: src/List.hpp

//[guard]
//[includes]
class List {
    void add(int value);
    void remove(int index);
    void clear();
};

现在这三个函数将有独立的文件:

文件: src/清单/add.cpp

void List::add(int value) {
    // Do something
}

想象另一个

这些将在某个时刻编译,头文件其他编译的类中使用

假设另一个名为ABC的类使用List的头文件。对于类ABC中的每个函数,生成一个对象文件。

现在我们要调整List的标题,我们不想改变函数,我们只想添加一个函数:

文件: src/List.hpp

//[guard]
//[includes]
class List {
    void add(int value);
    int find(int value);
    void remove(int index);
    void clear();
};

另一个源文件和目标文件正在生成,在这个例子中叫做:src/List/find.cpp和src/List/find.o

现在我的问题,这是一个合法的方式来使用头,源和对象吗?这是否会产生问题,或者这根本不可能?

另外,类ABC中名为List的类是否仍然与新创建的名为List的类相同?

你的设计看起来可行。然而,我不会推荐它。而且你没有提到模板或标准容器。

我感觉

  • 实际上很重要(出于效率的原因)有很多(通常是小的)inline函数,特别是内联成员函数(如getter, setter等),通常包含在它们的class { ....中}定义。

  • 因此,一些成员函数应该是inline,或者在类中,如

    class Foo { 
      int _x;
      Foo(int x) : _x(x) {};
      ~Foo() { _x=0; };
      int f(int d) const { return _x + d; };
    }
    

则所有的构造函数Foo::Foo(int),析构函数Foo::~Foo和成员函数int Foo::f(int)都内联

或在类之后(对于机器生成的代码通常更容易),如

    class Foo {
       int _x;
       inline Foo(int x);
       inline ~Foo();
       inline int f(int d) const;
    };
    Foo::Foo(int x) { _x = x; };
    Foo::~Foo() { _x = 0; };
    int Foo::f(int d) const { return _x+d; };

在这两种情况下你都需要内联(或者链接时间优化例如gcc -flto -O编译&链接),以提高效率。

编译器只有在知道函数的定义(函数体)时才能内联函数。

  • 那么,每次你#include一些类的定义。您需要以某种方式编译内联函数定义。要么把它放在同一个头文件中,要么头文件本身应该#include其他文件(提供内联函数的定义)

  • 一般来说,特别是当使用标准c++库(和使用标准容器,例如#include <vector>)时,你会得到很多系统头文件(间接包含)。在实践中,您不想要非常小的实现文件(即每个文件有几十行源代码是不切实际的)。

  • 同样,现有的c++框架库也会拉出很多(间接的)头文件(例如#include <QtGui>带来了大量的代码)。

  • 我建议至少有一千行c++源文件(*.hh*.cc)。

查看预处理代码的大小,例如使用g++ -H -C -E…你会在实践中感到害怕:即使在编译一个只有几十行源代码的小c++文件时,你也会有数千行预处理的源代码行。

因此我建议千行源文件:任何使用c++标准库或一些c++框架库(Boost, Qt)的小文件都是从间接包含的文件中提取大量源行。

参见这个答案,为什么Google(与D.Novillo一起)努力将准备好的头文件添加到GCC中,为什么LLVM/Clang(与C. latner一起)想要C和c++中的模块。为什么Ocaml、Rust、Go……模块…

您还可以查看GCC生成的GIMPLE表示,或者使用MELT探针(MELT是扩展GCC的特定领域语言,探针是一个简单的图形界面,用于检查GCC的一些内部表示,如GIMPLE),或者使用-fdump-tree-all选项来检查GCC(小心:该选项产生数百个转储文件)。您也可以将-ftime-report传递给GCC,以便在编译c++代码时更多地了解它在哪里传递时间。

对于机器生成的c++代码,我建议生成更少的文件,但使它们更大。生成数千个几十行的c++小文件是低效的(使总构建时间太长):编译器将花费大量时间一次又一次地解析相同的#include -d系统头文件,并实例化相同的模板类型(例如,当使用标准容器时)很多次。

请记住,c++允许每个源文件有几个类(与Java相反(内部类除外))。

同样,如果你所有的c++代码都生成了,你真的不需要生成头文件(或者你可能生成一个大的*.hh),因为你的生成器应该知道哪些类&函数在每个生成的*.cc中真正使用,并且只能在该文件中生成有用的声明和内联函数定义。

注:注意inline(和register一样)只是对编译器的一个(有用的)提示。它可以避免内联标记为inline的函数(即使在class定义中是隐式的)。它也可以内联一些没有标记为inline的函数。然而,编译器需要知道函数体以内联它。

我相信一个函数是否内联是由编译器决定的(见问题我如何知道内联函数是否在它被调用的地方被实际替换?)。要内联一个函数(尽管这并不一定意味着该函数在编译时绝对会内联),您应该在其类中定义该函数,或者在头文件中定义该函数之前使用"inline"命令。例如:

inline int Foo::f(int d) const { return _x+d; };

是的,这很好。这就是静态库的实现方式,因为这使得链接器更容易不拉入不使用的东西。