Boost C++ 库

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

Boost.Flyweight 教程:配置 Boost.Flyweight



目录

Boost.Flyweight 的可配置方面

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

自由顺序模板参数接口

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

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 是相同的享元类型,导致这两个类的值存储在同一个享元工厂中,尽管它们各自的范围预计不会重叠。标记可以用来将它们变成真正不同的类型

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 是不同的享元类,它们各自有单独的工厂。标记是一种纯粹的语法设备:任何类型都可以用于 tag 结构中的标记,尽管良好的风格建议使用具有描述性名称的标记类,这些名称对于正在定义享元类型的上下文是本地的。

工厂规范

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

flyweight<T,hashed_factory<> >

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

工厂配置中涉及的类型

给定的 flyweight 实例具有关联的 flyweight::key_typeflyweight::value_type 类型(在常规享元的情况下它们相等,如果使用 键值享元,则它们不同)。此外,还有一个内部 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 是享元的键类型,如上所述,boost::mpl::_1 代表存储在工厂中的元素的内部 Entry 类型。假设我们想为一个 std::string 享元配置 hashed_factory,使用特殊的哈希谓词 special_hash 和自定义分配器 custom_allocator;这将被指定如下

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 中并发容器的工厂。该工厂特别适合在多线程场景中创建享元,因为它不需要外部同步或跟踪;因此,它通常应与 no_lockingno_tracking 一起使用

typedef flyweight<
  std::string,
  concurrent_factory<>,
  no_locking,
  no_tracking
> concurrent_string_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 的有序容器来实现享元工厂。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;

持有者规范

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

static_holder

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

这是 Boost.Flyweight 的默认持有者指定符,它生成的持有者,其唯一的工厂作为程序的局部静态变量存在。

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 使用的享元工厂对象与主程序中看到的工厂对象不同:因此,str1str2 内部指向的值表示形式将不同,并且会被错误地认为不相等。由于工厂重复,可能会出现许多其他问题,包括未定义的行为。

intermodule_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 复制)