Boost C++ 库

...世界上最受推崇和设计最精良的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ 编码标准

Boost.Flyweight 教程:配置 Boost.Flyweight



目录

Boost.Flyweight 的可配置方面

大多数情况下,flyweight 的默认配置已经足够好,用户无需关心对其 flyweight 实例的进一步调整;然而,当需要更多地控制 Boost.Flyweight 的行为时,提供了全面的机制来选择、配置甚至扩展以下实现方面

自由顺序模板参数接口

flyweight 类模板具有“智能”规范接口,通过该接口,配置方面可以作为可选模板参数以用户喜欢的任何顺序提供。例如,带有 基于集合的工厂无跟踪 的标记 std::stringflyweight 可以像这样指定

flyweight<std::string, tag<label_t>,  set_factory<>, no_tracking   >

或者像这样

flyweight<std::string, no_tracking,   tag<label_t>,  set_factory<> >

或以任何其他顺序;规范中只需要 std::string 占据第一个位置。

头文件包含

介绍部分中显示的示例代码使用了 "boost/flyweight.hpp" 便利头文件,它只是简单地包含了类模板 flyweight 及其默认配置组件的头文件

#include <boost/flyweight/flyweight.hpp>      // class template flyweight
#include <boost/flyweight/hashed_factory.hpp> // hashed flyweight factory
#include <boost/flyweight/static_holder.hpp>  // regular factory instantiation
#include <boost/flyweight/simple_locking.hpp> // simple locking policy
#include <boost/flyweight/refcounted.hpp>     // refcounting tracking policy

当使用这些组件以外的组件时,必须显式包含其特定的头文件。

标记

考虑以下两种类型

typedef flyweight<std::string> name_t;
typedef flyweight<std::string> ip_address_t;

尽管从技术上讲这两种类型是相同的,但这仅仅是巧合,因为名称和 IP 地址之间没有合理的关联。在内部,name_tip_address_t 是相同的 flyweight 类型这一事实导致这两类的值存储在同一个 flyweight 工厂中,尽管它们的各自范围预计不会重叠。标记 可以用来将它们变成真正不同的类型

struct name_tag{};
typedef flyweight<std::string,tag<name_tag> > name_t;

struct ip_address_tag{};
typedef flyweight<std::string,tag<ip_address_tag> > ip_address_t;

现在,name_tip_address_t 是不同的 flyweight 类,每个类都有单独的工厂。标记是一种纯粹的语法手段:任何类型都可以用于 tag 构造内部的标记,尽管良好的风格建议使用描述性名称的标记类,这些标记类是 flyweight 类型定义上下文的本地类。

工厂规范

flyweight 使用一种称为工厂的内部组件,其目的是存储和检索 flyweight 对象在给定时间引用的不同值。默认情况下,使用基于哈希容器的工厂,因此 flyweight<T> 实际上等同于

flyweight<T,hashed_factory<> >

其中 hashed_factory 是所谓的工厂说明符。Boost.Flyweight 提供了几个预定义的工厂说明符,它们不仅允许用户选择使用的工厂的具体类型,还接受它们自己的模板参数来定制每个工厂。

工厂配置中涉及的类型

给定的 flyweight 实例具有关联的 flyweight::key_typeflyweight::value_type 类型(在常规 flyweight 的情况下它们相等,如果使用键值 flyweight,则它们不同)。此外,还有一个内部 Entry 类型,它对应于实际存储在工厂中的对象的类型:Entry 包含 flyweight 的共享 value_type 对象以及一些内部簿记信息;此外,Entry 可以隐式转换为 const key_type&,以便工厂可以依赖 key_type 来查找 Entry。由于 Entryflyweight 实现的内部类型,因此用户无法在工厂的配置中直接引用它。相反,可以使用代理 占位符 类型 boost::mpl::_1

hashed_factory

头文件: "boost/flyweight/hashed_factory.hpp"
语法: hashed_factory<[Hash[,Pred[,Allocator]]]>

此说明符是 Boost.Flyweight 默认使用的,它控制对内部基于哈希容器的工厂的使用。通过 BinaryPredicate Pred 来确定值是否等效,并使用 Hash 将其索引到工厂容器中,Hash 假定是类型为 Key 的参数的 Hash 函数对象。Allocator 参数由工厂容器用于其内存分配需求。这些参数的默认类型使得表达式

flyweight<T,hashed_factory<> >

等同于

flyweight<
  T,
  hashed_factory<
    boost::hash<key_value>,
    std::equal_to<key_value>,
    std::allocator<boost::mpl::_1>
  >
>

其中 key_type 是 flyweight 的键类型,boost::mpl::_1 如上所述,代表工厂中存储元素的内部 Entry 类型。假设我们想要为 std::string flyweight 配置带有特殊哈希谓词 special_hash 和自定义分配器 custom_allocatorhashed_factory;这将按如下方式指定

flyweight<
  std::string,
  hashed_factory<
    special_hash<std::string>,
    std::equal_to<key_value>,
    custom_allocator<boost::mpl::_1>
  >
>

concurrent_factory

头文件: "boost/flyweight/concurrent_factory.hpp"
语法: concurrent_factory<[Hash[,Pred[,Allocator]]]>

此说明符提供了一个基于 Boost.Unordered 中的并发容器的工厂。该工厂特别适用于多线程场景中的 flyweight 创建,因为它不需要外部同步或跟踪;因此,它通常应与 no_lockingno_tracking 结合使用

typedef flyweight<
  std::string,
  concurrent_factory<>,
  no_locking,
  no_tracking
> concurrent_string_flyweight;

未使用的值(那些不再被任何 flyweight 引用的值)由内置垃圾回收器定期从工厂中删除。

concurrent_factory 参数的默认类型使得表达式

flyweight<T,concurrent_factory<> >

等同于

flyweight<
  T,
  concurrent_factory<
    boost::hash<key_value>,
    std::equal_to<key_value>,
    std::allocator<boost::mpl::_1>
  >
>

hashed_factory 中的含义相同。

set_factory

头文件: "boost/flyweight/set_factory.hpp"
语法: set_factory<[Compare[,Allocator]]>

set_factory 借助于 std::set 类型的有序容器来实现 flyweight 工厂。Compare 必须是一个 BinaryPredicate,它在 flyweight 作用的值类型上诱导严格弱排序;与 STL 有序容器的习惯一样,如果根据 Pred,没有一个值小于另一个值,则认为两个值是等效的。Allocator 是一种分配器类型,它传递给工厂内部容器以执行其内存相关任务。当使用默认参数时,表达式

flyweight<T,set_factory<> >

等同于

flyweight<
  T,
  set_factory<std::less<key_type>,std::allocator<boost::mpl::_1> >
>

set_factoryhashed_factory 之间进行选择时,有序容器和哈希容器比较中通常出现的权衡也适用:因此,基于集合的值查找和插入通常比基于哈希的查找和插入慢,但后者可能会受到病态的最坏情况场景的影响,性能非常差。

assoc_container_factory

头文件: "boost/flyweight/assoc_container_factory.hpp"
语法: assoc_container_factory<ContainerSpecifier>

此说明符可以看作是 hashed_factoryset_factory 的泛化,用户可以在其中提供工厂所基于的容器的确切类型。对于那些不熟悉 Boost MPL 库 的人来说,容器的指定方式起初可能看起来有点令人生畏:ContainerSpecifier 必须是一个 MPL Lambda Expression,这样,当使用 上面解释的类型 Entrykey_type 调用它时,它会生成一个 Entry 元素的容器类型,该容器类型满足以下要求

  1. 容器类型必须是 AssociativeContainerUnorderedAssociativeContainer 的模型,具有唯一键,其中 Entry 的等效性由条目可转换成的 key_type 值确定。
  2. 容器必须是稳定的,即,其迭代器在插入和擦除操作后必须保持有效。请注意,许多现有的哈希容器实现不满足此条件,这些实现在重新哈希操作时会使迭代器无效。

让我们通过一个例子来看看容器说明符是什么样的。假设我们有我们自己的有序容器,如下所示

template<
  typename Elem,
  typename Compare=std::less<Elem>,
  typename Allocator=std::allocator<Elem>
>
class ultrafast_set
{
  ...
};

然后,ultrafast_set 可以像这样插入到 assoc_container_factory

typedef flyweight<
  std::string,
  assoc_container_factory<
    // MPL lambda expression follows
    ultrafast_set<mpl::_1,std::less<std::string> >
  >
> flyweight_string;

如已解释,mpl::_1 是一个所谓的 MPL 占位符,充当“槽”,将被 Boost.Flyweight 的内部机制替换为 Entry。请注意,我们没有依赖 ultrafast_setCompare 的默认参数,而是为 std::string 提供了固定的实例化:这是因为要求声明 ContainerSpecifier 将在内部填充的类型可转换为 const key_type&(此处为 const std::string&),并且查找和条目的等效性应基于 key_type 来确定。另一方面,Allocator 参数的默认参数工作正常,如果我们显式地写下来,则更明显

typedef flyweight<
  std::string,
  assoc_container_factory<
    ultrafast_set<
      mpl::_1,
      std::less<std::string>,
      std::allocator<mpl::_1>
    >
  >
> flyweight_string;

Holder 规范

每个 flyweight 类型,即类模板 flyweight 的每个不同实例,都与一个工厂对象相关联。在大多数情况下,此工厂对象是如何创建的对于 Boost.Flyweight 的用户来说并不重要,但是在某些特殊情况下,控制这方面是必要的。一个名为holder的内部组件负责实例化工厂类和一些其他内部信息;此组件由holder 说明符指定,static_holder 是默认的 holder 说明符。

static_holder

头文件: "boost/flyweight/static_holder.hpp"
语法: static_holder

这是 Boost.Flyweight 的默认 holder 说明符,并生成 holder,其中唯一的工厂作为程序的本地静态变量存在。

intermodule_holder

头文件: "boost/flyweight/intermodule_holder.hpp"
语法: intermodule_holder

在大多数 C++ 环境中,静态变量与动态加载的模块不能很好地混合,因为相同的静态变量的实例可以在不同的模块中重复,即使根据定义,该变量应该是唯一的。在许多情况下,如果模块不使用受影响的类型相互通信,则这种重复不会被注意到,但考虑一下这种通信确实发生的情况

// module 1

typedef flyweight<std::string> flyweight_string;

// produce_string is exported so that it can be dynamically
// linked

flyweight_string produce_string()
{
  return flyweight_string("boost");
}
// main program

typedef flyweight<std::string> flyweight_string;

int main()
{
  ... // import module 1

  flyweight_string str1=produce_string();
  flyweight_string str2("boost");
  assert(str1==str2);
}

在许多环境中,此程序会导致断言失败,因为模块 1 中看到的 flyweight_string 使用的 flyweight 工厂对象与主程序中看到的工厂对象不是同一个工厂对象:因此,str1str2 在内部指向的值表示将不同,并且会被错误地认为不相等。由于工厂重复,可能会出现许多其他问题,包括未定义的行为。

intermodule_holder 指定一个工厂 holder,它可以避免重复问题并确保程序的所有模块都使用相同的工厂实例。要修复上面的示例,只需在两个模块中将 flyweight_string 重新定义为

typedef flyweight<std::string,intermodule_holder> flyweight_string;

intermodule_holder 在编译时间方面比 static_holder 繁琐得多,并且在程序启动时引入了不可忽略的开销,因此其使用应保留在真正必要的情况下。

锁定策略

与每个 flyweight 类型关联的内部工厂是一个共享资源,因此在多线程环境中必须正确同步对其的访问。锁定策略指定用于此目的的同步机制。

simple_locking

头文件: "boost/flyweight/simple_locking.hpp"
语法: simple_locking

这是默认的锁定策略。它指定操作系统提供的最简单的本机同步原语(如果可用)。

no_locking

头文件: "boost/flyweight/no_locking.hpp"
语法: no_locking

不强制执行同步,因此允许无限制地内部访问实现共享资源。选择 no_locking 会导致比默认的 simple_locking 稍快的执行速度,但它会使类型变为线程不安全,这可能会导致灾难性的后果。除非在单线程环境中,或者绝对保证特定的 flyweight 类型不会在并发场景中使用,否则不应使用此策略。

跟踪策略

跟踪策略控制 flyweight 对象的生命周期,并可以基于此信息采取行动。例如,合适的跟踪机制可以确定何时可以安全地擦除工厂中存储的给定值,因为它不再被任何 flyweight 引用;这正是默认跟踪策略 refcounted 所做的。

refcounted

头文件: "boost/flyweight/refcounted.hpp"
语法: refcounted

此跟踪策略确定存储在工厂中的值配备引用计数机制,以便当最后一个与其关联的 flyweight 对象被销毁时,工厂条目被擦除。

no_tracking

头文件: "boost/flyweight/no_tracking.hpp"
语法: no_tracking

当选择此策略时,不进行 flyweight 跟踪,这意味着存储在工厂中的值将保留在其中直到程序终止。与 refcounted 相比,no_tracking 具有优点和缺点。优点是

而使用 no_tracking 的潜在缺点包括




修订于 2024 年 9 月 27 日

© 版权所有 2006-2024 Joaquín M López Muñoz。根据 Boost 软件许可协议 1.0 版分发。(请参阅随附文件 LICENSE_1_0.txt 或在 https://boost.ac.cn/LICENSE_1_0.txt 复制副本)