Boost C++ 库

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

PrevUpHomeNext

C++11/C++14/C++17 一致性

移动和就地构造
有状态分配器
作用域分配器
关联容器中的插入提示以及为具有等效键的元素保留插入顺序
初始化列表
空前向迭代器
多态内存资源
forward_list<T>
vector vs. std::vector 异常保证
可以通过常量引用更改的参数
vector<bool> 特化
使用 std::memset 的非标准值初始化

Boost.Container 旨在实现完全的 C++11 一致性,除了有充分理由的偏差外,并尽可能多地向后移植到 C++03。 显然,这种一致性是一个正在进行的工作,因此本节解释了实现了哪些 C++11/C++14/C++17 功能,以及其中哪些功能已向后移植到早期的标准兼容编译器。

对于具有右值引用的编译器以及使用 Boost.Move 右值引用模拟的 C++03 类型,Boost.Container 支持所有与移动语义相关的 C++11 功能:容器是可移动的,value_type 的要求是 C++11 容器指定的那些。

对于具有可变参数模板的编译器,Boost.Container 支持来自 C++11 的就地插入(emplace, ...)函数。 对于那些不支持可变参数模板的编译器,Boost.Container 使用预处理器创建一组重载,最多达到有限数量的参数。

C++03 对有状态分配器不友好。 为了容器对象的紧凑性和简单性,它不要求容器支持具有状态的分配器:分配器对象不需要存储在容器对象中。 存储具有状态的分配器是不可能的,例如,一个分配器,它保存指向从中分配内存的区域的指针。 C++03 允许实现者假设同一类型的两个分配器始终比较相等(这意味着由一个分配器对象分配的内存可以由同一类型的另一个实例释放),并且当容器被交换时,分配器不会被交换。

C++11 通过 std::allocator_traits 进一步改进了有状态分配器支持。 std::allocator_traits 是容器和分配器之间的协议,分配器编写者可以自定义其行为(容器是否应在移动构造函数、交换等中传播它?)遵循 allocator_traits 要求。 Boost.Container 不仅在 C++11 中支持此模型,而且还通过 boost::container::allocator_traits 将其向后移植到 C++03,包括一些 C++17 更改。 此类为 C++03 编译器提供了一些解决方法,以实现与 std::allocator_traits 相同的分配器保证。

在 [Boost.Container] 容器中,如果可能,则保存单个分配器以构造 value_type。 如果容器需要辅助分配器(例如,dequestable_vector 使用的数组分配器),则该分配器也存储在容器中,并在容器构造时从用户提供的分配器初始化(即,它不是在需要辅助内存时动态构造的)。

C++11 通过引入 std::scoped_allocator_adaptor 类模板改进了有状态分配器。 scoped_allocator_adaptor 使用一个外部分配器和零个或多个内部分配器实例化。

作用域分配器是一种机制,用于以受控方式自动将分配器的状态传播到容器的子对象。 如果仅使用一个分配器类型实例化,则内部分配器将变为 scoped_allocator_adaptor 本身,从而为容器和容器内的每个元素以及元素本身是容器的情况,递归地为其每个元素使用相同的分配器资源。 如果使用多个分配器实例化,则第一个分配器是容器使用的外部分配器,第二个分配器传递给容器元素的构造函数,如果元素本身是容器,则第三个分配器传递给元素的元素,依此类推。

Boost.Container 实现了自己的 scoped_allocator_adaptor 类,并且 也将此功能向后移植到 C++03 编译器。 由于 C++03 的限制,在这些编译器中,scoped_allocator_adaptor::construct 函数实现的分配器传播将基于 N2554: 作用域分配器模型 (Rev 2) 提案 中提出的特征(constructible_with_allocator_suffixconstructible_with_allocator_prefix)。 在符合标准的 C++11 编译器或支持 SFINAE 表达式的编译器中(当 BOOST_NO_SFINAE_EXPR 未定义时),特征将被忽略,并且 C++11 规则(is_constructible<T, Args..., inner_allocator_type>::valueis_constructible<T, allocator_arg_t, inner_allocator_type, Args...>::value)将用于检测分配器是否必须使用后缀或前缀分配器参数传播。

LWG 问题 #233 纠正了 C++98 中的缺陷,并指定了等效键如何在关联容器中插入。 Boost.Container 实现了 N1780 关于 LWG 问题 233 的评论:关联容器中的插入提示 中指定的 C++11 更改

  • a_eq.insert(t): 如果 a_eq 中存在包含等效于 t 的元素的范围,则 t 将插入到该范围的末尾。
  • a_eq.insert(p,t): t 将尽可能靠近紧靠 p 之前的位置插入。

Boost.Container 在实现此功能的编译器中支持从初始化列表进行初始化、赋值和插入。

Boost.Container 实现了 C++14 空前向迭代器,这意味着值初始化的迭代器可以与其他相同类型的值初始化的迭代器进行比较并比较相等。 值初始化的迭代器的行为就像它们引用了同一个空序列的末尾一样(示例取自 N3644)

vector<int> v = { ... };
auto ni = vector<int>::iterator();
auto nd = vector<double>::iterator();
ni == ni; // True.
nd != nd; // False.
v.begin() == ni; // ??? (likely false in practice).
v.end() == ni;   // ??? (likely false in practice).
ni == nd; // Won't compile.

库基础扩展 (最终草案) 文档包含提供分配器类型擦除和运行时多态性的类。 正如该提案的作者 Pablo Halpern 在论文 (N3916 多态内存资源 (r2)) 中解释的那样

C++ 中有效内存管理的一个重大障碍是在非泛型上下文中使用分配器的能力不足。 在大型软件系统中,大多数应用程序程序由编译一次并多次链接的非泛型过程或面向对象的代码组成。

然而,C++ 中的分配器历来仅依赖于编译时多态性,因此不适合在词汇类型中使用,词汇类型通过单独编译的模块之间的接口传递,因为分配器类型必然会影响使用它的对象的类型。 此提案建立在 C++11 中对分配器所做的改进之上,并描述了一组用于运行时多态内存资源的工具,这些工具与现有的编译时多态分配器互操作。

来自 Fundamentals TS 的大多数实用程序都已合并到 C++17 中,但 Boost.Container 为 C++03、C++11 和 C++14 编译器提供了它们。

Boost.Container 实现了提案中几乎所有命名空间 boost::container::pmr 下的类。 分为两组,

Boost.Container 的多态资源库可从 C++03 容器中使用,并且如果 库基础 规范所需的 C++11 功能不可用,则提供一些替代实用程序。

让我们回顾一下 N3916 中给出的用法示例,看看如何使用 Boost.Container 实现它: 假设我们正在处理一系列购物清单,其中购物清单是字符串的容器,并将它们存储在购物清单的集合(列表)中。 每个正在处理的购物清单都使用有限数量的内存,这些内存在短时间内需要,而购物清单的集合使用无限数量的内存,并将存在更长的时间。 为了提高效率,我们可以为临时购物清单使用基于有限缓冲区的更节省时间的内存分配器。

让我们看看如何定义 ShoppingList 以支持可以从不同底层机制分配内存的多态内存资源。 最重要的细节是

  • 它应声明支持分配器,该分配器定义 allocator_type typedef。 此 allocator_type 的类型将为 memory_resource *,它是多态资源的基础类。
  • 它必须定义将分配器作为参数的构造函数。 它可以通过两种方式实现

注意: 在 C++03 编译器中,需要程序员将 true 专门化为 constructible_with_allocator_suffixconstructible_with_allocator_prefix,因为在 C++03 中,无法在编译时自动检测所选选项。 如果未进行专门化,Boost.Container 假定后缀选项

//ShoppingList.hpp
#include <boost/container/pmr/vector.hpp>
#include <boost/container/pmr/string.hpp>

class ShoppingList
{
   // A vector of strings using polymorphic allocators. Every element
   // of the vector will use the same allocator as the vector itself.
   boost::container::pmr::vector_of
      <boost::container::pmr::string>::type m_strvec;
   //Alternatively in compilers that support template aliases:
   //    boost::container::pmr::vector<boost::container::pmr::string> m_strvec;
   public:

   // This makes uses_allocator<ShoppingList, memory_resource*>::value true
   typedef boost::container::pmr::memory_resource* allocator_type;

   // If the allocator is not specified, "m_strvec" uses pmr::get_default_resource().
   explicit ShoppingList(allocator_type alloc = 0)
      : m_strvec(alloc) {}

   // Copy constructor. As allocator is not specified,
   // "m_strvec" uses pmr::get_default_resource().
   ShoppingList(const ShoppingList& other)
      : m_strvec(other.m_strvec) {}

   // Copy construct using the given memory_resource.
   ShoppingList(const ShoppingList& other, allocator_type a)
      : m_strvec(other.m_strvec, a) {}

   allocator_type get_allocator() const
   { return m_strvec.get_allocator().resource(); }

   void add_item(const char *item)
   { m_strvec.emplace_back(item); }

   //...
};

然而,这种节省时间的分配器不适用于寿命更长的购物清单集合。 此示例展示了如何使用使用节省时间的分配器的临时购物清单来填充使用通用分配器的寿命长的购物清单集合,如果没有多态分配器,这将非常困难。

Boost.Container 中,对于节省时间的分配,我们可以使用 monotonic_buffer_resource,提供一个外部缓冲区,该缓冲区将一直使用到耗尽为止。 在默认配置中,当缓冲区耗尽时,将改为使用默认内存资源。

#include "ShoppingList.hpp"
#include <cassert>
#include <boost/container/pmr/list.hpp>
#include <boost/container/pmr/monotonic_buffer_resource.hpp>

void processShoppingList(const ShoppingList&)
{  /**/   }

int main()
{
   using namespace boost::container;
   //All memory needed by folder and its contained objects will
   //be allocated from the default memory resource (usually new/delete) 
   pmr::list_of<ShoppingList>::type folder; // Default allocator resource
   //Alternatively in compilers that support template aliases:
   //    boost::container::pmr::list<ShoppingList> folder;
   {
      char buffer[1024];
      pmr::monotonic_buffer_resource buf_rsrc(&buffer, 1024);

      //All memory needed by temporaryShoppingList will be allocated
      //from the local buffer (speeds up "processShoppingList")
      ShoppingList temporaryShoppingList(&buf_rsrc);
      assert(&buf_rsrc == temporaryShoppingList.get_allocator());

      //list nodes, and strings "salt" and "pepper" will be allocated
      //in the stack thanks to "monotonic_buffer_resource".
      temporaryShoppingList.add_item("salt");
      temporaryShoppingList.add_item("pepper");
      //...

      //All modifications and additions to "temporaryShoppingList"
      //will use memory from "buffer" until it's exhausted.
      processShoppingList(temporaryShoppingList);

      //Processing done, now insert it in "folder",
      //which uses the default memory resource
      folder.push_back(temporaryShoppingList);
      assert(pmr::get_default_resource() == folder.back().get_allocator());
      //temporaryShoppingList, buf_rsrc, and buffer go out of scope
   }
   return 0;
}

请注意,folder 中的购物清单使用默认分配器资源,而购物清单 temporaryShoppingList 使用寿命短但速度非常快的 buf_rsrc。 尽管使用了不同的分配器,但您可以将 temporaryShoppingList 插入到文件夹中,因为它们具有相同的 ShoppingList 类型。 此外,虽然 ShoppingList 直接使用 memory_resource,但 pmr::list, pmr::vectorpmr::string 都使用 polymorphic_allocator

传递给 ShoppingList 构造函数的资源将传播到向量和该 ShoppingList 中的每个字符串。 同样,用于构造 folder 的资源将传播到插入到列表中的 ShoppingList 的构造函数(以及这些 ShoppingLists 中的字符串)。 polymorphic_allocator 模板旨在几乎可以与指向 memory_resource 的指针互换,从而在分配器的模板策略样式和分配器的多态基类样式之间产生 桥梁

此示例实际上展示了即使在 C++03 编译器中,使用 Boost.Container 编写类型擦除的、支持分配器的类是多么容易。

Boost.Container 尚未提供 C++11 forward_list 容器,但它将在未来的版本中提供。

vector 不支持 std::vectorinsert, push_back, emplace, emplace_back, resize, reserveshrink_to_fit 等函数中给出的强异常保证,无论是对于可复制类还是无异常移动类。 在 C++11 中,move_if_noexcept 用于维护 C++03 异常安全保证以及 C++11 移动语义。 这种强异常保证降低了可复制类型和抛出异常的可移动类型的插入性能,当使用上述成员将此类类型插入向量时,会将移动降级为复制。

这种强异常保证也排除了使用某些类型的就地重新分配的可能性,这可以进一步提高 vector 的插入性能。 请参阅 扩展分配器 以了解有关这些优化的更多信息。

vector 始终使用移动构造函数/赋值来重新排列向量中的元素,并在分配器支持的情况下使用内存扩展机制,同时仅提供基本的安全保证。 它牺牲了异常保证以换取更高的性能。

几个容器操作使用一个可以通过常量引用获取的参数,该参数可以在函数执行期间更改。 LWG 问题 526 (如果标准中的函数更改参数是否未定义?) 讨论了它们

//Given std::vector<int> v
v.insert(v.begin(), v[2]);
//v[2] can be changed by moving elements of vector

//Given std::list<int> l:
l.remove(*l.begin())
//The operation could delete the first element, and then continue trying to access it.

通过的决议 NAD(不是缺陷)意味着以前的操作必须是明确定义的。 这需要代码检测对插入元素的引用以及在这种情况下的额外副本,即使不使用对已插入对象的引用,也会影响性能。 请注意,采用右值引用或迭代器范围的等效函数需要元素未在容器中插入。

Boost.Container 优先考虑性能,并且未实现 NAD 决议:在可能修改参数的函数中,库需要对未存储在容器中的元素的引用。 使用对插入元素的引用会导致未定义的行为(尽管在调试模式下,可以通过 BOOST_ASSERT 通知此前提条件违规)。

vector<bool> 特化一直存在问题,并且已经进行了几次不成功的尝试,试图弃用或从标准中删除它。 Boost.Container 没有实现它,因为有更优越的 Boost.DynamicBitset 解决方案。 有关 vector<bool> 的问题,请参阅以下论文

引言

  • 但是 C++ 委员会给这个优秀的数据结构命名为 vector<bool> 是一个遗憾,并且它没有对需要为此数据结构优化的关键通用算法提供指导或鼓励。 因此,很少有 std::lib 实现会为此费心。
  • 在 1998 年,承认委员会犯了一个错误是有争议的。 从那时起,Java 不得不弃用其库的如此重要的部分,以至于 C++ 会因弃用单个次要模板特化而受到嘲笑的想法似乎很古怪。
  • vector<bool> 不是容器,并且 vector<bool>::iterator 不是随机访问迭代器(甚至不是前向或双向迭代器)。 这已经在现场以神秘的方式破坏了用户代码。
  • vector<bool> 通过将其写入标准,对所有用户强制执行特定的(并且可能是糟糕的)优化选择。 优化是过早的; 不同的用户有不同的要求。 这也已经伤害了被迫实施解决方法以禁用“优化”的用户(例如,通过使用 vector<char> 并手动转换为/从 bool 转换)。

因此,boost::container::vector<bool>::iterator 返回真实的 bool 引用,并作为完全兼容的容器工作。 如果您需要 boost::container::vector<bool> 的内存优化版本,请使用 Boost.DynamicBitset

Boost.Container 使用零值的 std::memset 来初始化某些类型,因为在大多数平台上,此初始化会产生期望的值初始化,并提高了性能。

根据 C11 标准,Boost.Container 假定 对于任何整数类型,所有位均为零的对象表示形式应为该类型中值零的表示形式。 由于 _Bool/wchar_t/char16_t/char32_t 在 C 中也是整数类型,因此它认为所有 C++ 整数类型都可以通过 std::memset 初始化。

默认情况下,Boost.Container 还认为浮点类型可以使用 std::memset 初始化。 大多数平台都与此初始化兼容,但如果不需要此初始化,用户可以在包含库头文件之前 #define BOOST_CONTAINER_MEMZEROED_FLOATING_POINT_IS_NOT_ZERO

默认情况下,它还认为指针类型(指针和函数指针类型,不包括成员对象指针和成员函数指针)可以使用 std::memset 初始化。 大多数平台都与此初始化兼容,但如果不需要此初始化,用户可以在包含库头文件之前 #define BOOST_CONTAINER_MEMZEROED_POINTER_IS_NOT_ZERO


PrevUpHomeNext