Boost C++ 库

世界上最受推崇、设计最精良的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ Coding Standards

教程 - Boost C++ 函数库
PrevUpHomeNext

(代码示例来自 basic_base.cpp。)

想象一下,我们在 C++ 中开发一款角色扮演游戏,其中精灵(sprites)将在屏幕上渲染;为便于说明,我们可以将渲染简单地建模为向 std::ostream 输出一些信息。

struct sprite
{
  sprite(int id):id{id}{}
  virtual ~sprite()=default;
  virtual void render(std::ostream& os)const=0;

  int id;
};

游戏包含战士、重战士(一种特殊的战士)和地精,每种都由其自身类表示,这些类直接或间接派生自 sprite

struct warrior:sprite
{
  using sprite::sprite;
  warrior(std::string rank,int id):sprite{id},rank{std::move(rank)}{}

  void render(std::ostream& os)const override{os<<rank<<" "<<id;}

  std::string rank="warrior";
};

struct juggernaut:warrior
{
  juggernaut(int id):warrior{"juggernaut",id}{}
};

struct goblin:sprite
{
  using sprite::sprite;
  void render(std::ostream& os)const override{os<<"goblin "<<id;}
};

让我们用各种精灵填充一个多态集合。

#include <boost/poly_collection/base_collection.hpp>
...

boost::base_collection<sprite> c;

std::mt19937                 gen{92748}; // some arbitrary random seed
std::discrete_distribution<> rnd{{1,1,1}};
for(int i=0;i<8;++i){        // assign each type with 1/3 probability
  switch(rnd(gen)){
    case 0: c.insert(warrior{i});break;
    case 1: c.insert(juggernaut{i});break;
    case 2: c.insert(goblin{i});break;
  }
}

这里有两点需要注意:

  • boost::base_collection 没有像 std::vector 那样的 push_back 成员函数,因为元素的存储顺序不能由用户代码自由选择——稍后我们会对此进行更多介绍。插入机制更像是 std::unordered_multiset 这样的容器,即带有或不带位置提示insertemplace
  • 元素不是用 new 创建的,而是构建在栈上并直接传递,就像使用标准的非多态容器一样。
[Important] 重要提示

插入到 boost::base_collection(或 Boost.PolyCollection 的其他容器)中的元素必须是可复制和可赋值的;严格来说,它们至少必须模拟 MoveConstructible,并且要么是 MoveAssignable,要么在移动构造时不抛出异常。这可能会迫使您重新审视代码,因为通常会在虚继承层次的基层显式禁止复制,以避免 切片

现在可以通过对 c 进行简单的 for 循环来实现渲染。

const char* comma="";
for(const sprite& s:c){
  std::cout<<comma;
  s.render(std::cout);
  comma=",";
}
std::cout<<"\n";

输出为:

juggernaut 0,juggernaut 4,juggernaut 7,goblin 1,goblin 3,goblin 5,warrior 2,warrior 6

正如我们所预料的,遍历顺序与插入顺序不符。相反,元素根据其具体类型分组在 分段 中。这里我们看到重战士在前,然后是地精和战士。总的来说,不应假定分段顺序,在您的环境中,该示例的分段顺序可能不同。另一方面,插入到现有分段中的元素始终放在最后(除非提供了提示)。例如,在插入一个后到的 id==8 的地精后,

c.insert(goblin{8});

渲染循环的输出是(新元素以红色显示):

juggernaut 0,juggernaut 4,juggernaut 7,goblin 1,goblin 3,goblin 5,goblin 8,warrior 2,warrior 6

删除可以按通常方式进行。

// find element with id==7 and remove it
auto it=std::find_if(c.begin(),c.end(),[](const sprite& s){return s.id==7;});
c.erase(it);

现在渲染的输出为:

juggernaut 0,juggernaut 4,goblin 1,goblin 3,goblin 5,goblin 8,warrior 2,warrior 6

(代码示例来自 basic_function.cpp。使用了 C++14 的 std::make_unique。)

在游戏开发过程中,产品经理要求在渲染循环中添加两种新类型的实体:

  • 叠加消息,例如分数,建模为 std::string
  • 来自第三方库的弹出窗口,不幸的是,它们不派生自 sprite,并且使用它们自己的 display 成员函数进行渲染。
struct window
{
  window(std::string caption):caption{std::move(caption)}{}

  void display(std::ostream& os)const{os<<"["<<caption<<"]";}

  std::string caption;
};

于是我们决定重构代码,将精灵、消息和窗口存储在专用容器中,

std::vector<std::unique_ptr<sprite>> sprs;
std::vector<std::string>             msgs;
std::vector<window>                  wnds;

并将我们的渲染容器定义为 可调用实体——函数指针或具有函数调用 operator() 的对象——接受 std::ostream& 作为参数的 boost::function_collection

#include <boost/poly_collection/function_collection.hpp>
...

// function signature accepting std::ostream& and returning nothing
using render_callback=void(std::ostream&);

boost::function_collection<render_callback> c;

然后,我们用适合精灵、std::string 和窗口的适配器来填充它。Lambda 函数允许代码非常简洁。

auto render_sprite(const sprite& s){
  return [&](std::ostream& os){s.render(os);};
}

auto render_message(const std::string& m){
  return [&](std::ostream& os){os<<m;};
}

auto render_window(const window& w){
  return [&](std::ostream& os){w.display(os);};
}
...

for(const auto& ps:sprs)c.insert(render_sprite(*ps));
for(const auto& m:msgs)c.insert(render_message(m));
for(const auto& w:wnds)c.insert(render_window(w));

渲染循环现在看起来像这样:

const char* comma="";
for(const auto& cbk:c){
  std::cout<<comma;
  cbk(std::cout);
  comma=",";
}
std::cout<<"\n";

并且在精灵、消息和窗口的特定场景下产生以下输出:

juggernaut 0,goblin 1,warrior 2,goblin 3,"stamina: 10,000","game over",[pop-up 1],[pop-up 2]

我们刚刚创建的容器在许多方面都类似于 std::vector<std::function<render_callback>>,主要区别在于,对于 boost::function_collection,相同类型的可调用实体在内存中被打包在一起 [11],从而提高了性能(这也是使用 Boost.PolyCollection 的目的),而 std::function 的向量则为存储的每个实体进行单独分配 [12]。事实上,boost::function_collection 中保存的 value_type 元素实际上**不是** std::function,尽管它们行为类似,并且在需要时可以转换为 std::function

auto cbk=*c.begin();
cbk(std::cout); // renders first element to std::cout
std::function<render_callback> f=cbk;
f(std::cout);   // exactly the same

这样做的原因是:多态集合的元素不能由用户随意赋值,因为这可能最终尝试将一个对象插入到不同类型的分段中。所以,与 std::function 不同,下面这样做是行不通的:

*c.begin()=render_message("last minute message"); // compile-time error

(代码示例来自 basic_any.cpp。)

[Note] 注意

这里我们只触及了 Boost.TypeErasure 的基本要素,这些要素对于介绍 boost::any_collection 是必需的。建议读者阅读 Boost.TypeErasure 的文档以获取更多信息。

在衡量了最新更改的性能后,我们发现渲染速度太慢,于是决定再次重构:如果我们能将所有实体——精灵、消息和窗口——存储在一个容器中,就能消除一个间接层次。问题是,这些类型彼此之间完全不相关。

Boost.TypeErasure 提供了一个类模板 boost::type_erasure::any<Concept>,只要对象符合 Concept 指定的接口,它就可以容纳任意类型的对象。在我们的例子中,我们发现通过提供 operator<< 的重载来实现一个通用的渲染接口特别容易。

std::ostream& operator<<(std::ostream& os,const sprite& s)
{
  s.render(os);
  return os;
}

// std::string already has a suitable operator<<

std::ostream& operator<<(std::ostream& os,const window& w)
{
  w.display(os);
  return os;
}

这样我们就可以用它来指定所有实体必须遵守的接口:

#include <boost/poly_collection/any_collection.hpp>
#include <boost/type_erasure/operators.hpp>
...

using renderable=boost::type_erasure::ostreamable<>;
boost::any_collection<renderable> c;

刚刚创建的集合可以愉快地接受异构实体的插入(因为接口符合性在编译时被静默检查)。

// populate with sprites
std::mt19937                 gen{92748}; // some arbitrary random seed
std::discrete_distribution<> rnd{{1,1,1}};
for(int i=0;i<4;++i){        // assign each type with 1/3 probability
  switch(rnd(gen)){
    case 0: c.insert(warrior{i});break;
    case 1: c.insert(juggernaut{i});break;
    case 2: c.insert(goblin{i});break;
  }
}

// populate with messages
c.insert(std::string{"\"stamina: 10,000\""});
c.insert(std::string{"\"game over\""});

// populate with windows
c.insert(window{"pop-up 1"});
c.insert(window{"pop-up 2"});

渲染变成:

const char* comma="";
for(const auto& r:c){
  std::cout<<comma<<r;
  comma=",";
}
std::cout<<"\n";

输出为:

[pop-up 1],[pop-up 2],juggernaut 0,goblin 1,goblin 3,warrior 2,"stamina: 10,000","game over"

boost::function_collection 一样,这个容器类似于 std::vector<boost::type_erasure::any<Concept>>,但由于相同类型的元素打包在一起,性能更好。同样,boost::any_collection<Concept>value_type **不是** boost::type_erasure::any<Concept>,而是一个行为类似且具有相应功能的实体 [13]。无论如何,我们不再通过抽象的 sprite& 访问精灵,因此原则上我们可以拆除虚继承层次结构并独立实现每个类型:这留给读者作为练习。

(代码示例来自 basic_variant.cpp。C++17 用于 overloaded 实用程序。)

在这一点上,我们已经有了这样一组不相关的异构类型,我们也可以在编译时指定它们:

#include <boost/poly_collection/variant_collection.hpp>
...

boost::variant_collection<
  boost::mp11::mp_list<warrior,juggernaut,goblin,elf,std::string,window>
> c;

需要 boost::mp11::mp_list 来在语法上区分集合中的类型与(默认的)分配器参数。如果未指定特殊分配器,则别名模板 variant_collection_of 提供了稍微方便一些的语法:

boost::variant_collection_of<
  warrior,juggernaut,goblin,elf,std::string,window
> c;

boost::variant_collection 的行为类似于 std::vectorstd::variant,只是相同类型的对象会被分组在一起。对象通过访问进行访问:

// usual utility to construct a visitor
template<typename... Ts>
struct overloaded:Ts...{using Ts::operator()...;};
template<class... Ts>
overloaded(Ts...)->overloaded<Ts...>;

const char* comma="";
for(const auto& r:c){
  std::cout<<comma;
  visit(overloaded{
    [](const sprite& s)       { s.render(std::cout); },
    [](const std::string& str){ std::cout<<str; },
    [](const window& w)       { w.display(std::cout); }
  },r);
  comma=",";
}
std::cout<<"\n";

输出:

warrior 1,warrior 4,juggernaut 2,goblin 0,elf 3,"stamina: 10,000","game over",[pop-up 1],[pop-up 2]

替代的访问函数 visit_by_index 可能更方便,因为它不需要我们构建重载的访问器:

auto print_sprite=[](const sprite& s)       { s.render(std::cout); };
auto print_string=[](const std::string& str){ std::cout<<str; };
auto print_window=[](const window& w)       { w.display(std::cout); };

const char* comma="";
for(const auto& r:c){
  std::cout<<comma;
  visit_by_index(
    r,
    print_sprite,  // type #0: warrior
    print_sprite,  // type #1: juggernaut
    print_sprite,  // type #2: goblin
    print_sprite,  // type #3: elf
    print_string,  // type #4
    print_window); // type #5
  comma=",";
}
std::cout<<"\n";

请注意,类型会按照指定的完全相同的顺序进行遍历。boost::variant_collection<boost::mp11:mp_list<Ts..>>value_type 行为类似于 std::variant<Ts..>,但有一些明显的限制,例如无法更改包含对象类型的能力(例如,未提供 std::variant::emplace 的等效方法)。

(代码示例来自 segmented_structure.cpp。使用了 C++14 的 std::make_unique。)

回到我们 boost::base_collection 的例子,假设我们想这样重构填充代码:

std::unique_ptr<sprite> make_sprite()
{
  static std::mt19937                 gen{92748};
  static std::discrete_distribution<> rnd{{1,1,1}};
  static int                          id=0;

  switch(rnd(gen)){
    case 0: return std::make_unique<warrior>(id++);break;
    case 1: return std::make_unique<juggernaut>(id++);break;
    case 2: return std::make_unique<goblin>(id++);break;
  }
}
...

for(int i=0;i<8;++i)c.insert(*make_sprite());
// throws boost::poly_collection::unregistered_type

出乎意料的是,这段代码抛出了一个类型为 boost::poly_collection::unregistered_type 的异常。与我们原始代码相比,有什么变化?

假设一个 warrior 是由 make_sprite 创建的。语句 c.insert(*make_sprite()) 将对象作为 sprite& 传递:即使 boost::base_collection 足够智能,知道该对象实际上派生自 sprite(通过使用 typeid())并且要避免 切片,也无法在不**在编译时**访问 warrior 类型来正确实例化内部类模板的情况下,为它创建一个分段 [14]。这在重构前的代码中没有发生,因为对象是作为对其真实类型的引用传递的。

当一个类型被注册到多态集合中时,意味着为它创建了一个(可能是空的)分段。可以通过以下方式检查:

std::cout<<c.is_registered<warrior>()<<"\n";       // prints 0
std::cout<<c.is_registered(typeid(warrior))<<"\n"; // alternate syntax

注册会在对象作为其真实类型的引用传递时或emplace 时自动发生,并使用 register_types 显式进行。

c.register_types<warrior,juggernaut,goblin>();
// everything works fine now
for(int i=0;i<8;++i)c.insert(*make_sprite());

一旦 T 被注册到多态集合中,它将保持注册状态,无论类型为 T 的对象是否被存储,除非集合被移动、赋值或交换。

由于切片主要是面向对象编程中的一个问题,因此使用 boost::function_collectionboost::any_collection 时,预期的 unregistered_type 异常会少得多。尽管如此,还是可能出现人为构造的例子:

boost::any_collection<renderable> c1,c2;
... // populate c2

c1.insert(*c2.begin()); // throws: actual type of *c2.begin() not known by c1
[Note] 注意

boost::variant_collection 的类型注册方式不同:由于可接受的类型在编译时指定,因此在集合构造时会自动创建其关联的分段;因此,不提供 is_registeredregister_types

对于多态集合的大部分接口,可以通过提供其类型或 typeid() 来仅引用给定分段的元素。例如:

... // populate c with 8 assorted entities

std::cout<<c.size()<<"\n";                    // 8 sprites
std::cout<<c.size<juggernaut>()<<"\n";        // 2 juggernauts
std::cout<<c.size(typeid(juggernaut))<<"\n";  // alternate syntax
c.clear<juggernaut>();                        // remove juggenauts only
std::cout<<c.empty<juggernaut>()<<"\n";       // 1 (no juggernauts left)
std::cout<<c.size()<<"\n";                    // 6 sprites remaining

请注意,其中任何一个(除了 reserve)如果类型未注册,都会抛出 boost::poly_collection::unregistered_type 异常。对于 boost::variant_collection,我们使用类型的索引号而不是 typeid()

boost::variant_collection<
  boost::mp11::mp_list<warrior,juggernaut,goblin,elf,std::string,window>
> c3;
... // populate c3 some entities

std::cout<<c3.size<warrior>()<<"\n"; // same as with other collections
std::cout<<c3.size(0)<<"\n";         // list number of warriors (type #0 in the list of types)

分段特定的可寻址性还包括遍历:

以下代码仅遍历集合中的 warrior

const char* comma="";
for(auto first=c.begin(typeid(warrior)),last=c.end(typeid(warrior));
    first!=last;++first){
  std::cout<<comma;
  first->render(std::cout);
  comma=",";
}
std::cout<<"\n";

输出:

warrior 2,warrior 6

begin|end(typeid(T)) 返回 local_base_iterator 类型的对象,该对象与 T 的分段相关联。这些迭代器的解引用值与普通迭代器相同(对于 boost::base_collection<base>,为 base&),但只能用于遍历给定分段(例如,来自不同分段的 local_base_iterator 无法相互比较)。作为交换,local_base_iterator 是一个 RandomAccessIterator,而整个集合的迭代器仅模拟 ForwardIterator

使用方便的 segment 成员函数,可以使用更简洁的范围 for 循环:

const char* comma="";
for(const auto& x:c.segment(typeid(warrior))){
  std::cout<<comma;
  x.render(std::cout);
  comma=",";
}
std::cout<<"\n";

local_base_iterator 更强大的是 local_iterator<T>

const char* comma="";
for(auto first=c.begin<warrior>(),last=c.end<warrior>();
    first!=last;++first){
  first->rank.insert(0,"super");
  std::cout<<comma;
  first->render(std::cout);
  comma=",";
}
std::cout<<"\n";

// range-based for loop alternative

const char* comma="";
for(auto& x:c.segment<warrior>()){
  x.rank.insert(0,"super");
  std::cout<<comma;
  x.render(std::cout);
  comma=",";
}
std::cout<<"\n";

这会将 "super" 前缀添加到现有战士的 rank 数据成员中:

superwarrior 2,superwarrior 6

细心的读者会注意到,为了访问 rank(它是 warrior 的成员,而不是其基类 sprite),first(或范围 for 循环版本中的 x)必须引用一个 warrior&,而这正是 local_iterator<warrior>(由 begin<warrior>() 返回的类型)和 local_base_iterator 之间的区别。local_iterator<warrior> 也是一个 RandomAccessIterator:在大多数方面,[begin<T>(), end<T>()) 可以视为一个 T 对象数组的范围。local_iterator<T> 可以显式转换为 local_base_iterator。反之,如果一个 local_base_iteratorT 的分段相关联,那么它可以显式转换为 local_iterator<T>(否则转换是未定义行为)。

当使用 boost::variant_collection 时,local_iterator<T> 直接引用 T 对象的事实更加明显。

// traverse all windows in our variant_collection

for(auto& x:c3.segment<window>()){
  // visitation not required: we're accessing window directly
  x.display(std::cout);
}

该图显示了多态集合的所有迭代器(不包括 const_ 版本)的作用域。

我们还没有描述图的底部部分。虽然 segment(typeid(T)) 用于遍历特定分段的元素,但 segment_traversal() 返回一个用于遍历分段的对象,因此整个集合可以通过嵌套的分段-元素 for 循环来处理,如下所示:

const char* comma="";
for(auto seg:c.segment_traversal()){
  for(sprite& s:seg){
    std::cout<<comma;
    s.render(std::cout);
    comma=",";
  }
}
std::cout<<"\n";

遍历循环的分段分解构成了性能改进的算法的基础。

std::vector 类似,可以提前为分段预留内存,以最大限度地减少重新分配。

c.reserve<goblin>(100); // no reallocation till we exceed 100 goblins
std::cout<<c.capacity<goblin>()<<"\n"; // prints 100

如果不存在指定类型(此处为 goblin)的分段,则会自动创建一个。这与分段特定成员函数(会抛出 boost::poly_collection::unregistered_type)的其余部分形成对比。

reserve 可以一次处理所有分段。以下代码:

c.reserve(1000); // reserve(1000) for each segment
std::cout<<c.capacity<warrior>()<<", "
         <<c.capacity<juggernaut>()<<", "
         <<c.capacity<goblin>()<<"\n"; // prints 1000, 1000, 1000

指示每个已存在的分段预留 1000 个元素。如果稍后创建分段(通过元素插入或 类型注册),其容量将不同。

[Note] 注意

与标准容器不同,不提供集合级别的 capacity()max_size(),因为它们的常规语义无法应用于 Boost.PolyCollection:例如,capacity() 通常用于检查在不重新分配的情况下可以插入多少元素,但在分段结构中,这取决于元素的具体类型以及它们是否已注册。

(代码示例来自 insertion_emplacement.cpp。)

我们已经知道,在没有进一步的位置信息的情况下,insert(x)x 存储在其关联分段的末尾。当提供常规迭代器 it 时,如果 itx 属于同一分段,则插入发生在指定位置;否则,it 将被忽略。例如,如果我们的精灵集合包含以下实体:

juggernaut 0,juggernaut 4,juggernaut 7,goblin 1,goblin 3,goblin 5,warrior 2,warrior 6

那么这段代码

c.insert(c.begin(),juggernaut{8});

将新的 juggernaut 放在开头。

juggernaut 8,juggernaut 0,juggernaut 4,juggernaut 7,goblin 1,...

而位置提示

c.insert(c.begin(),goblin{9});

将被忽略,新的 goblin 将被插入到其分段的末尾。

juggernaut 8,...,juggernaut 7,goblin 1,goblin 3,goblin 5,goblin 9,warrior 2,...

还可以使用局部迭代器来指示插入位置。

c.insert(c.begin<juggernaut>()+2,juggernaut{10});
                           // ^^ remember local iterators are random access
juggernaut 8,juggernaut 0,juggernaut 10,juggernaut 4,juggernaut 7,goblin 1,...

不过,有一个注意事项:在使用局部迭代器时,插入的元素必须属于指定的分段

c.insert(c.begin(typeid(warrior)),juggernaut{11}); // undefined behavior!!

提供了用于范围插入的成员函数,带或不带位置提示。

boost::base_collection<sprite> c2;

c2.insert(c.begin(),c.end()); // read below

// add some more warriors
std::array<warrior,3> aw={{11,12,13}};
c2.insert(aw.begin(),aw.end());

// add some goblins at the beginning of their segment
std::array<goblin,3> ag={{14,15,16}};
c2.insert(c2.begin<goblin>(),ag.begin(),ag.end());

前面已经解释过的,必须注意,如果类型不是以其真实类型的引用传递,则它们必须预先注册到集合中。那么,为什么这行代码

c2.insert(c.begin(),c.end());

没有抛出 boost::poly_collection::unregistered_type 异常?碰巧,在插入范围属于相同类型的多态集合的特殊情况下,注册会自动进行 [15]

对于 Boost.PolyCollection 而言,构造(emplacement)与标准容器略有不同。考虑这个构造 goblin 的尝试:

c.emplace(11); // does not compile

仔细观察,上面的代码无法编译是很自然的:Boost.PolyCollection 无法知道我们想要构造的正是 goblin,而不是其他可以从 int 构造的类型(例如 warriorjuggernaut 或不相关的类)。因此,必须显式指定被构造元素的类型:

c.emplace<goblin>(11); // now it works

insert 一样,可以为构造指定位置:

c.emplace_hint<juggernaut>(c.begin(),12); // at the beginning if possible
c.emplace_pos<goblin>(c.begin<goblin>()+2,13); // amidst the goblins
c.emplace_pos<warrior>(c.begin(typeid(warrior)),14); // local_base_iterator

注意这里的命名:当使用常规迭代器指定位置时使用 emplace_hint,而对于局部迭代器,则使用 emplace_pos

(代码示例来自 exceptions.cpp。)

除了 std::bad_alloc 和用户提供的类型生成的异常之外,多态集合还可以抛出以下异常:

  • boost::poly_collection::unregistered_type
  • boost::poly_collection::not_copy_constructible
  • boost::poly_collection::not_equality_comparable

第一种异常抛出的情况已在前面讨论过;下面我们重点关注后两种。

我们在游戏中有一个新的精灵类型,由不可复制的类 elf 定义:

struct elf:sprite
{
  using sprite::sprite;
  elf(const elf&)=delete; // not copyable
  elf(elf&&)=default;     // but moveable
  void render(std::ostream& os)const override{os<<"elf "<<id;}
};

并且我们使用它没有问题,直到我们写下这个:

c.insert(elf{0}); // no problem
...

c2=c; // throws boost::poly_collection::not_copy_constructible

第一次插入成功,因为传递的 elf 对象是临时的,并且可以移动到容器中,但第二条语句实际上需要将 c 中的 elf 元素复制c2,因此抛出异常。

这种行为可能令人惊讶的是,标准容器通过编译时失败来发出此类问题的信号。在这里,我们负担不起这种奢侈,因为多态集合中包含的精确类型直到运行时才知道(例如,如果在复制 cc2 之前 elf 元素被擦除,一切都会成功):基本上,将错误从编译时推迟到运行时是动态多态的一个内在特征。

同样,相等比较要求元素本身是可相等比较的:

c.clear<elf>(); // get rid of non-copyable elfs
c2=c;           // now it works
// check that the two are indeed equal
std::cout<<(c==c2)<<"\n";
                // throws boost::poly_collection::not_equality_comparable

一旦我们注意到我们没有为任何 sprite 定义 operator==,上面的内容就没什么特别的了。然而,这个问题可能要过很长时间才会被注意到,因为判断两个多态集合是否相等(即它们所有非空分段是否相等)可以在不进行任何比较的情况下返回 false(例如,如果分段大小不同),在这种情况下不会抛出异常。

[Note] 注意

不提供 <<=>>= 比较的运算符,是因为分段顺序不是固定的,并且在不同但相同的集合之间可能有所不同。情况类似于标准 无序关联容器

这三个是 Boost.PolyCollection 抛出的所有内在异常。这意味着,如果一个类型是 CopyConstructibleMoveAssignable(或移动构造不抛出)并且是 EqualityComparable,那么 Boost.PolyCollection 的整个接口对其都是无限制可用的 [16]

(代码示例来自 algorithms.cpp。使用了 C++14 的泛型 lambda 表达式。C++17 用于 overloaded 实用程序。)

Boost.PolyCollection 的最终目的是加速处理大量多态实体,特别是对于涉及线性遍历的操作,这些操作通常通过 for 循环或使用典型的 std::for_each 算法来实现。

const char* comma="";
std::for_each(c.begin(),c.end(),[&](const sprite& s){
  std::cout<<comma;
  s.render(std::cout);
  comma=",";
});
std::cout<<"\n";

将程序中使用的容器从经典替代方案替换为 Boost.PolyCollection:

  • std::vector<base*> (或类似)→ boost::base_collection<base>
  • std::vector<std::function<signature>>boost::function_collection<signature>
  • std::vector<boost::type_erasure::any<concept_>>boost::any_collection<concept_>
  • std::vector<std::variant<Ts...>>boost::any_collection<boost::mp11::mp_list<Ts...>>

预计会因为更好的缓存和分支预测行为而提高性能。但仍有改进空间。

考虑使用双分段-元素循环(基于 Boost.PolyCollection 的局部迭代器功能)来转换上面的代码:

const char* comma="";
for(auto seg_info:c.segment_traversal()){
  for(const sprite& s:seg_info){
    std::cout<<comma;
    s.render(std::cout);
    comma=",";
  }
}
std::cout<<"\n";

尽管乍一看并不明显,但同一基本操作的这个版本实际上比第一个版本更快:对于像 Boost.PolyCollection 使用的分段结构,非局部迭代器传递给 std::for_each 的每次迭代都涉及:

  1. 跳到分段中的下一个位置;
  2. 检查分段结束(总是);
  3. 如果到达末尾(不频繁),则选择下一个分段;
  4. 检查范围结束(总是);

而在第二个版本中,内层循环(大部分处理发生的地方)的迭代是一个简单的递增和检查操作,也就是说,少了一次检查(这次检查发生在短得多的外层循环)。当算法的工作负载(对元素本身进行实际有用的操作)相对较轻时,循环的开销可能非常显著。

为了让用户更容易利用更快的“分段-元素”循环,Boost.PolyCollection 提供了自己的 for_each 版本,基于该技术:

#include <boost/poly_collection/algorithm.hpp>
...

const char* comma="";
boost::poly_collection::for_each(c.begin(),c.end(),[&](const sprite& s){
  std::cout<<comma;
  s.render(std::cout);
  comma=",";
});
std::cout<<"\n";

boost::poly_collection::for_each 具有与 std::for_each 相同的接口和行为,除了它只适用于多态容器的(非局部)迭代器 [17]。其他标准算法的版本也提供了:

auto n=boost::poly_collection::count_if(
  c.begin(),c.end(),[](const sprite& s){return s.id%2==0;});
std::cout<<n<<" sprites with even id\n";

实际上,对于 <algorithm> 中的大多数(但不是全部)与多态容器兼容的算法,都提供了变体 [18]。详情请参阅参考

所谓类型恢复,是指通过提供缺失的类型信息,从抽象实体获取具体实体的通用过程。

sprite*  ps=new warrior{5};
// sprite -> warrior
warrior* pw=static_cast<warrior*>(ps);

boost::type_erasure::any<renderable> r=std::string{"hello"};
// renderable -> std::string
std::string& str=boost::type_erasure::any_cast<std::string&>(r);

类型恢复有潜力提高性能。例如,考虑以下代码:

// render r with std::string restitution
if(boost::type_erasure::typeid_of(r)==typeid(std::string)){
  std::string& str=boost::type_erasure::any_cast<std::string&>(r);
  std::cout<<str<<"\n";
}
else{
  std::cout<<r<<"\n";
}

行为上,这段代码等同于简单地执行 std::cout<<r<<"\n",但当类型恢复成功时,语句 std::cout<<str<<"\n" 就跳过了一个虚拟调用(如果使用 r 的话,原本会发生该调用)。从一般角度来看,向编译器提供额外的类型信息允许其执行比抽象情况更多的优化,其中内联是首要的例子。

所有 Boost.PolyCollection 算法都接受一个可选的类型列表用于恢复。让我们以 boost::any_collection 的场景为例来说明这一点:

const char* comma="";
boost::poly_collection::for_each
  <warrior,juggernaut,goblin>( // restituted types
  c.begin(),c.end(),[&](const auto& x){ // loop traverses *all* elements
    std::cout<<comma<<x;
    comma=",";
  });
std::cout<<"\n";

输出:

warrior 2,warrior 6,[pop-up 1],[pop-up 2],juggernaut 0,juggernaut 4,
juggernaut 7,goblin 1,goblin 3,goblin 5,"stamina: 10,000","game over"

这个渲染循环与不进行类型恢复的循环在两个方面有所不同:

  • 作为算法的模板参数提供了一组类型。这表明这些类型可能实际上存在于集合中,但这并非承诺。此外,类型列表不必详尽无遗,也就是说,集合中可能存在未列出的类型——在上面的例子中,循环遍历所有元素(包括 std::stringwindow),而不仅仅是与恢复类型相对应的元素。总的来说,恢复的类型越多,潜在的性能提升就越大。
  • 传递的 lambda 函数是通用的,它接受其参数为 const auto& [19]

在内部,boost::poly_collection::for_each 检查每个分段,看其类型(例如 T)是否在类型恢复列表中:如果是,则 lambda 函数接收到的不是通用的 const boost::any_collection::value_type&,而是 const T&。对于每种恢复的类型,我们都节省了间接调用,并可能获得内联优化等。正如我们在性能部分中看到的,速度提升可能非常显著。

类型恢复对 Boost.PolyCollection 的其他集合同样有效。但是,在使用 boost::base_collection 时,性能提升的情况更为棘手:

const char* comma="";
boost::poly_collection::for_each<warrior,juggernaut,goblin>(
  c.begin(),c.end(),[&](const auto& s){
    std::cout<<comma;
    s.render(std::cout);
    comma=",";
  });
std::cout<<"\n";

这里的问题是,即使传递给 lambda 函数的参数(在适当时)会被恢复为 warrior&juggernaut&goblin&,使用它仍然涉及在 s.render(std::cout) 中进行虚函数调用。该调用是否解析为非虚调用取决于编译器的实现质量。

  • 如果具体类被标记为 final,编译器原则上拥有足够的信息来消除虚函数调用。
  • 除此之外,虚函数消除(devirtualization) 功能可能能够找出类型恢复场景实际上是将元素转换为其真实类型,在这种情况下,同样不需要虚函数调用。

在使用 boost::variant_collection 时,便利标记 all_types 要求恢复所有类型,而无需显式列出它们。这当然是可行的,因为这些类型再次在编译时已知。请注意,由于元素是根据其类型直接访问的,因此在示例中没有调用 visit [20]

boost::variant_collection<
  boost::mp11::mp_list<warrior,juggernaut,goblin,elf,std::string,window>
> c;
...

auto print_sprite=[](const sprite& s)       { s.render(std::cout); };
auto print_string=[](const std::string& str){ std::cout<<str; };
auto print_window=[](const window& w)       { w.display(std::cout); };
auto print=overloaded{print_sprite,print_string,print_window};

const char* comma="";
boost::poly_collection::for_each<boost::poly_collection::all_types>(
  c.begin(),c.end(),[&](const auto& r){
    std::cout<<comma;
    print(r);
    comma=",";
});
std::cout<<"\n";


[11] 请注意,所有 sprites 都属于一个分段:这就是为什么地精 #1 和 #3 不相邻的原因。读者练习:修改示例代码,使精灵根据其具体类型进一步分段。

[12] 除非应用了小型缓冲区优化。

[13] 实际上,它对于某个内部定义的 Concept2boost::type_erasure::any<Concept2,boost::type_erasure::_self&>,它扩展了 Concept

[14] 如果这在概念上难以理解,可以考虑一个可能更明显的例子,即 warrior 定义在一个链接到主程序的动态模块中:boost::base_collection 的代码在链接之前就被编译了,它甚至不知道这个尚未可见的类的大小,因此很难为接收到的对象分配一个分段。

[15] 也就是说,Boost.PolyCollection 拥有足够的静态信息来执行类型注册,而无需用户进一步的协助。

[16] 当然,前提是该类型有权进入集合,即它派生自指定的基类,或具有指定的签名可调用,等等。

[17] 对于任何其他类型的迭代器,保证不会编译。

[18] 例如,不提供需要双向迭代器或更高类别算法的算法,因为多态集合只有前向迭代器。

[19] 这需要 C++14,但在 C++11 中可以通过提供一个功能相同但更繁琐的、具有模板化调用运算符的函数对象来实现相同效果。

[20] 避免访问(Visitation avoidance)实际上是闭合多态领域中虚函数消除的等效方法。


PrevUpHomeNext