38

我正在将我的 C++ 应用程序的一部分从使用较旧的 C 类型数组更改为模板化的 C++ 容器类。有关详细信息,请参阅此问题。虽然该解决方案运行良好,但我对模板化代码所做的每一个微小更改都会导致大量重新编译发生,从而大大减慢了构建时间。有没有办法从头文件中获取模板代码并返回到 cpp 文件中,以便小的实现更改不会导致重大重建?

4

6 回答 6

31

几种方法:

  • export 关键字理论上可以提供帮助,但它的支持很差,并在 C++11 中被正式删除。
  • 显式模板实例化(参见此处此处)是最直接的方法,如果您可以提前预测您将需要哪些实例化(并且如果您不介意维护此列表)。
  • 外部模板,已被多个编译器作为扩展支持。我的理解是 extern 模板不一定让您将模板定义移出头文件,但它们确实使编译和链接更快(通过减少必须实例化和链接模板代码的次数)。
  • 根据您的模板设计,您可能能够将其大部分复杂性转移到 .cpp 文件中。标准示例是一个类型安全的向量模板类,它只包装了一个类型不安全的向量void*;所有复杂性都void*存在于 .cpp 文件中的向量中。Scott Meyers 在Effective C++中给出了一个更详细的示例(第 2 版中的第 42 项,“明智地使用私有继承”)。
于 2010-05-13T14:33:04.067 回答
23

我认为一般规则适用。尽量减少部分代码之间的耦合。将太大的模板头分解成较小的函数组一起使用,这样就不必将整个内容包含在每个源文件中。

此外,尝试使标头快速进入稳定状态,也许可以针对较小的测试程序对其进行测试,这样在集成到较大的程序中时就不需要更改(太多)。

(与任何优化一样,在处理模板时优化编译器的速度可能更不值得,而不是首先找到一种“算法”优化来大幅减少工作量。)

于 2010-05-13T14:53:10.587 回答
9

首先,为了完整起见,我将介绍简单的解决方案:仅在必要时使用模板化代码,并将其基于非模板代码(在其自己的源文件中实现)。

但是,我怀疑真正的问题是您使用通用编程,因为您将使用典型的 OO 编程并最终得到一个臃肿的类。

举个例子:

// "bigArray/bigArray.hpp"

template <class T, class Allocator>
class BigArray
{
public:
  size_t size() const;

  T& operator[](size_t index);
  T const& operator[](size_t index) const;

  T& at(size_t index);
  T const& at(size_t index);

private:
  // impl
};

这让你震惊吗?可能不是。毕竟它看起来很简约。问题是,它不是。这些at方法可以在不失一般性的情况下被分解出来:

// "bigArray/at.hpp"

template <class Container>
typename Container::reference_type at(Container& container,
                                      typename Container::size_type index)
{
  if (index >= container.size()) throw std::out_of_range();
  return container[index];
}

template <class Container>
typename Container::const_reference_type at(Container const& container,
                                            typename Container::size_type index)
{
  if (index >= container.size()) throw std::out_of_range();
  return container[index];
}

好的,这会稍微改变调用:

// From
myArray.at(i).method();

// To
at(myArray,i).method();

但是,感谢 Koenig 的查找,只要将它们放在同一个命名空间中,就可以将它们称为不合格的,所以这只是一个习惯问题。

这个例子是人为的,但一般观点是成立的。请注意,由于它的通用性at.hpp永远不必包含bigArray.hpp并且仍然会像它是成员方法一样生成紧凑的代码,只是如果我们愿意,我们可以在其他容器上调用它。

现在,如果用户不使用它,BigArray则不需要包含at.hpp它......因此减少了她的依赖关系,并且如果您更改该文件中的代码也不会受到影响:例如更改std::out_of_range调用以显示文件名和行号,容器的地址、大小和我们尝试访问的索引。

另一个(不是那么明显)的优点是,如果BigArray违反了完整性约束,那么at显然是没有原因的,因为它不能与类的内部发生混淆,从而减少了嫌疑人的数量。

这是许多作者推荐的,例如C++ 编码标准中的 Herb Sutters :

第 44 条:更喜欢编写非成员非朋友函数

并且已经在Boost中广泛使用……但是你必须改变你的编码习惯!

然后当然你只需要包含你所依赖的东西,应该有静态 C++ 代码分析器报告包含但未使用的头文件,这可以帮助解决这个问题。

于 2010-05-13T15:36:56.300 回答
6
  • 您可以获得支持export关键字的编译器,但这不太可能持续下去。

  • 您可以使用显式实例化,但不幸的是,这需要您提前预测将使用的模板类型。

  • 如果您可以从算法中提取出模板类型,则可以将其放入自己的 .cc 文件中。

  • 我不建议这样做,除非这是一个主要问题,但是:您可以提供一个模板容器接口,该接口通过调用void*您可以随意更改的实现来实现。

于 2010-05-13T14:16:40.783 回答
4

使用模板作为解决问题的技术会导致编译速度变慢。一个典型的例子是 C 中的 std::sort 与 qsort 函数。这个函数的 C++ 版本需要更长的编译时间,因为它需要在每个翻译单元中进行解析,并且几乎每次使用这个函数都会创建一个不同的实例这个模板(假设闭包类型通常作为排序谓词提供)。

尽管这些放缓是意料之中的,但有一些规则可以帮助您编写高效的模板。其中四个描述如下。

基尔法则

下面介绍的 Chiel 规则描述了哪些 C++ 结构对于编译器来说是最困难的。如果可能,最好避免这些构造以减少编译时间。

以下 C++ 功能/结构按编译时间降序排列:

  • SFINAE
  • 实例化函数模板
  • 实例化一个类型
  • 调用别名
  • 向类型添加参数
  • 向别名调用添加参数
  • 查找记忆类型

在设计和开发 Boost.TMP 时使用了基于上述规则的优化。尽可能避免快速模板编译的顶级结构。

下面是一些示例,说明如何使用上面列出的规则。

减少模板实例化

让我们看一下std::conditional。它的声明是:

template< bool B, typename T, typename F >
struct conditional;

每当我们更改为该模板提供的三个参数中的任何一个时,编译器都必须创建它的一个新实例。例如,想象以下类型:

struct first{};
struct second{};

现在,以下所有内容将以不同类型的实例化结束:

using type1 = conditional<true, first, second>;
using type2 = conditional<true, second, first>;
std::is_same_v<type1, type2>; // it’s false

using type3 = conditional<false, first, second>;
using type4 = conditional<false, second, first>;
std::is_same_v<type1, type2>; // it’s false

我们可以通过将条件的实现更改为:

template <bool>
struct conditional{
     template <typename T, typename F>
     using type = T;
};

template <>
struct conditional<false>{
     template <typename T, typename F>
     using type = F;
};

在这种情况下,编译器将只为所有可能的参数创建两个“条件”类型的实例。有关此示例的更多详细信息,请查看Odin Holmes 关于 Kvasir 库的讨论

创建显式模板实例化

每当您怀疑某个模板的实例将被经常使用时,显式实例化它是一个好主意。通常,std::string是 的显式实例化std::basic_string<char>

为编译时算法创建专业化

Kvasir-MPL 专门针对长类型列表的算法来加速它们。你可以在这里看到一个例子。在这个头文件中,排序算法被手动专门用于 255 种类型的列表。手动专业化加快了长列表的编译。

于 2020-09-17T12:41:49.507 回答
3

您可以定义一个没有模板的基类并将大部分实现移到那里。然后,模板化数组将只定义代理方法,这些方法对所有内容都使用基类。

于 2010-05-13T14:20:55.330 回答