30

我已经创建了几个 C++ 库,它们目前是header-only。我的类的接口和实现都写在同一个.hpp文件中。

我最近开始认为这种设计不是很好:

  1. 如果用户想要编译库并动态链接它,他/她不能。
  2. 更改单行代码需要完全重新编译依赖于库的现有项目。

不过,我真的很喜欢仅标头库的各个方面:所有函​​数都可能被内联,并且它们非常容易包含在您的项目中 - 无需编译/链接任何东西,只需一个简单的#include指令。

有没有可能两全其美?我的意思是 - 允许用户选择他/她想如何使用图书馆。它还将加快开发速度,因为我会在“动态链接模式”下处理库以避免荒谬的编译时间,并以“仅头文件模式”发布我的成品以最大限度地提高性能。

第一个逻辑步骤是在文件中划分接口.hpp和实现。.inl

不过,我不确定如何前进。我已经看到许多库LIBRARY_API在它们的函数/类声明之前添加了宏——也许需要类似的东西来允许用户选择?


我所有的库函数都以inline关键字为前缀,以避免“...的多重定义”错误。我假设关键字将被文件LIBRARY_INLINE中的宏替换.inl?该宏将解析inline为“仅标题模式”,而对于“动态链接模式”则没有。

4

7 回答 7

19

初步说明:我假设一个 Windows 环境,但这应该很容易转移到其他环境。

您的图书馆必须为四种情况做好准备:

  1. 用作仅标头库
  2. 用作静态库
  3. 用作动态库(导入函数)
  4. 构建为动态库(导出函数)

因此,让我们为这些情况编写四个预处理器定义:INLINE_LIBRARYSTATIC_LIBRARYIMPORT_LIBRARYEXPORT_LIBRARY(这只是一个示例;您可能想要使用一些复杂的命名方案)。用户必须定义​​其中之一,具体取决于他/她想要什么。

然后你可以这样写你的标题:

// foo.hpp

#if defined(INLINE_LIBRARY)
#define LIBRARY_API inline
#elif defined(STATIC_LIBRARY)
#define LIBRARY_API
#elif defined(EXPORT_LIBRARY)
#define LIBRARY_API __declspec(dllexport)
#elif defined(IMPORT_LIBRARY)
#define LIBRARY_API __declspec(dllimport)
#endif

LIBRARY_API void foo();

#ifdef INLINE_LIBRARY
#include "foo.cpp"
#endif

你的实现文件看起来和往常一样:

// foo.cpp

#include "foo.hpp"
#include <iostream>

void foo()
{
    std::cout << "foo";
}

如果INLINE_LIBRARY定义了,则函数被声明为内联,并且实现像 .inl 文件一样被包含在内。

如果STATIC_LIBRARY已定义,则声明函数时不带任何说明符,并且用户必须将 .cpp 文件包含到他/她的构建过程中。

如果IMPORT_LIBRARY定义了,则导入函数,不需要任何实现。

如果EXPORT_LIBRARY已定义,则导出函数,用户必须编译这些 .cpp 文件。

在静态/导入/导出之间切换是很常见的事情,但我不确定将仅标题添加到等式中是否是一件好事。通常,有充分的理由来定义内联或不这样做。

就个人而言,我喜欢将所有内容都放入 .cpp 文件中,除非它真的必须被内联(如模板)或者它在性能方面有意义(非常小的函数,通常是单行)。这减少了编译时间和 - 更重要的是 - 依赖关系。

但是如果我选择内联定义一些东西,我总是把它放在单独的 .inl 文件中,只是为了保持头文件的干净和易于理解。

于 2014-09-05T09:56:39.940 回答
5

它是特定于操作系统和编译器的。在具有最新GCC编译器(4.9 版)的 Linux 上,您可能会使用过程间链接时间优化生成静态库。

这意味着您在编译和库链接时都使用库来构建库,并且在调用程序的编译和链接时都g++ -O2 -flto使用您的库。g++ -O2 -flto

于 2014-09-01T13:32:28.290 回答
2

Rationale

Put as little as necessary in header files and as much as possible in library modules, because of the very reasons that you mentioned: compile-time dependency and long compilation time. The only good reasons for header-only modules are:

  1. generic templates for user-defined template parameter;

  2. very short convenience functions when inlining gives significant performance.

In case 1, it is often possible to hide some functionality that does not depend on the user-defined type in a .cpp file.

Conclusion

If you stick to this rationale, then there is no choice: templated functionality that must allow user-defined types cannot be pre-compiled, but requires a header-only implementation. Other functionality should be hidden from the user in a library to avoid exposing them to the implementation details.

于 2014-09-06T09:49:53.343 回答
2

这是为了补充@Horstling 的回答。


您可以创建静态库或动态库。当您创建静态链接库时,所有函数/对象的编译代码都将保存到一个文件中(在 Windows 中扩展名为 .lib)。在主项目(使用库的项目)的链接时,这些代码将与主项目代码一起链接到您的最终可执行文件中。所以最终的可执行文件不会有任何运行时依赖。

动态链接库将在运行时(而不是链接时)合并到主项目中。编译库时,您会得到一个 .dll 文件(其中包含实际编译的代码)和一个 .lib 文件(其中包含足够的数据供编译器/运行时在 .dll 文件中查找函数/对象)。在链接时,可执行文件将被配置为加载 .dll 并根据需要使用来自该 .dll 的编译代码。您需要将 .dll 文件与可执行文件一起分发才能运行它。

设计库时无需在静态或动态链接(或仅标头)之间进行选择,您可以创建多个项目/makefile,一个用于创建静态 .lib,另一个用于创建 .lib/.dll 对,然后分发两种版本,供用户选择。(您需要使用@Horstling 建议的预处理器宏)。


您不能将任何模板放入预编译库中,除非您使用一种称为Explicit Instantiation的技术,该技术会限制模板参数。

另请注意,现代编译器/链接器通常不尊重 inline 修饰符。他们可以内联一个函数,即使它没有被指定为内联,或者可以动态调用另一个具有内联修饰符的函数,因为他们认为合适。(无论如何,我会建议在适用的地方明确放置 inline 以获得最大的兼容性)。因此,如果您使用静态链接库而不是仅包含标头的库(并启用编译器/链接器优化,当然),不会有任何运行时性能损失。正如其他人所建议的那样,对于肯定会从内联调用中受益的非常小的函数,最好将它们放在头文件中,因此动态链接库也不会遭受任何重大的性能损失。(无论如何,内联函数只会影响经常调用的函数的性能,


#include "foo.cpp"您可以更改 makefile/project 设置并将 foo.cpp 添加到要编译的源文件列表中,而不是将内联函数放入头文件(在您的头文件中)。这样,如果您更改任何函数实现,则无需重新编译整个项目,只会重新编译 foo.cpp。正如我之前提到的,优化编译器仍将内联您的小函数,您无需担心这一点。


如果您使用/设计预编译库,则应考虑使用与主项目不同的编译器版本编译库的情况。每个不同的编译器版本(甚至不同的配置,如 Debug 或 Release)都使用不同的 C 运行时(如 memcpy、printf、fopen 等)和 C++ 标准库运行时(如 std::vector<>、std::细绳, ...)。这些不同的库实现可能会使链接复杂化,甚至会产生运行时错误。

作为一般规则,请始终避免跨库共享编译器运行时对象(标准未定义的数据结构,如 FILE*),因为不兼容的数据结构会导致运行时错误。

链接项目时,C/C++ 运行时函数必须链接到库 .lib 或 .lib/.dll 或可执行文件 .exe。C/C++ 运行时本身可以链接为静态或动态库(您可以在 makefile/project 设置中进行设置)。

您会发现在库和主项目中动态链接到 C/C++ 运行时(即使您将库本身编译为静态库)避免了大多数链接问题(在多个运行时版本中存在重复的函数实现)。当然,您需要为所有使用的版本与您的可执行文件和库一起分发运行时 DLL。

有些场景需要静态链接到 C/C++ 运行时,在这些情况下,最好的方法是使用与主项目相同的编译器设置来编译库以避免链接问题。

于 2014-09-09T03:45:55.567 回答
1

Rather than a dynamic library, you could have a precompiled static library and thin header file. In an interactive quick build, you get the benefit of not having to recompile the world if implementation details changes. But a fully optimized release build can do global optimization and still figure out it can inline functions. Basically, with "link-time code generation" the toolset does the trick you were thinking about.

I'm familiar with Microsoft's compiler, which I know for sure does this as of Visual Studio 2010 (if not earlier).

于 2014-09-08T19:20:51.537 回答
1

模板化的代码必须只有头文件:为了实例化这个代码,类型参数必须在编译时知道。无法在共享库中嵌入模板代码。只有 .NET 和 Java 支持从字节码进行 JIT 实例化。

回复:非模板代码,对于简短的单行代码,我建议只保留标题。内联函数为编译器提供了更多优化最终代码的机会。

为了避免“疯狂的编译时间”,Microsoft Visual C++ 具有“预编译头文件”功能。我不认为 GCC 有类似的功能。

在任何情况下都不应内联长函数。

我有一个项目,其中包含仅标题位、编译库位和一些我无法确定属于何处的位。我最终得到了 .inc 文件,根据 #ifdef 有条件地包含在 .hpp 或 .cxx 中。说实话,该项目总是以“最大内联”模式编译的,所以过了一段时间我摆脱了 .inc 文件,只是将内容移动到 .hpp 文件中。

于 2014-09-08T19:24:59.520 回答
0

有没有可能两全其美?

就条款而言;由于工具不够智能,因此出现了限制。这个答案给出了当前的最大努力,仍然足够便携,可以有效地使用。

我最近开始认为这种设计不是很好。

应该是的。仅标头库是理想的,因为它们简化了部署:使该语言的重用机制与几乎所有其他语言的重用机制相似,这是明智的做法。但这是 C++。当前的 C++ 工具仍然依赖于半个世纪前的链接模型,这些模型消除了重要程度的灵活性,例如选择在单个级别上导入或导出哪些入口点,而无需被迫更改库的原始源代码。此外,C++ 缺乏适当的模块系统,并且仍然依赖美化的复制粘贴操作来工作(尽管这只是所讨论问题的一个附带因素)。

其实MSVC在这方面要好一点。它是唯一尝试在 C++ 中实现某种程度的模块化的主要实现(通过尝试例如C++ 模块)。它是唯一真正允许以下内容的编译器:

//// Module.c++
#pragma once
inline void Func() { /* ... */ }

//// Program1.c++
#include <Module.c++>
// Inlines or "vague" links Func(), whatever is better.
int main() { Func(); }

//// Program2.c++
// This forces Func() to be imported.
// The declaration must come *BEFORE* the definition.
__declspec(dllimport) __declspec(noinline) void Func();
#include <Module.c++>
int main() { Func(); }

//// Program3.c++
// This forces Func() to be exported.
__declspec(dllexport) __declspec(noinline) void Func();
#include <Module.c++>

请注意,这可用于有选择地从库中导入和导出单个符号,尽管仍然很麻烦。

GCC 也接受这一点(但必须更改声明的顺序),并且 Clang 没有任何方法可以在不更改库源的情况下达到相同的效果。

于 2016-12-28T17:06:53.437 回答