(代码示例来自 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
这样的容器,即带有或不带位置提示的 insert
和 emplace
。new
创建的,而是构建在栈上并直接传递,就像使用标准的非多态容器一样。![]() |
重要提示 |
---|---|
插入到 |
现在可以通过对 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
。)
![]() |
注意 |
---|---|
这里我们只触及了 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::vector
的 std::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_collection
和 boost::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
![]() |
注意 |
---|---|
|
对于多态集合的大部分接口,可以通过提供其类型或 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_iterator
与 T
的分段相关联,那么它可以显式转换为 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 个元素。如果稍后创建分段(通过元素插入或 类型注册),其容量将不同。
![]() |
注意 |
---|---|
与标准容器不同,不提供集合级别的 |
(代码示例来自 insertion_emplacement.cpp
。)
我们已经知道,在没有进一步的位置信息的情况下,insert(x)
将 x
存储在其关联分段的末尾。当提供常规迭代器 it
时,如果 it
和 x
属于同一分段,则插入发生在指定位置;否则,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
构造的类型(例如 warrior
、juggernaut
或不相关的类)。因此,必须显式指定被构造元素的类型:
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
,因此抛出异常。
这种行为可能令人惊讶的是,标准容器通过编译时失败来发出此类问题的信号。在这里,我们负担不起这种奢侈,因为多态集合中包含的精确类型直到运行时才知道(例如,如果在复制 c
到 c2
之前 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
(例如,如果分段大小不同),在这种情况下不会抛出异常。
![]() |
注意 |
---|---|
不提供 |
这三个是 Boost.PolyCollection 抛出的所有内在异常。这意味着,如果一个类型是 CopyConstructible
、MoveAssignable
(或移动构造不抛出)并且是 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
的每次迭代都涉及:
而在第二个版本中,内层循环(大部分处理发生的地方)的迭代是一个简单的递增和检查操作,也就是说,少了一次检查(这次检查发生在短得多的外层循环)。当算法的工作负载(对元素本身进行实际有用的操作)相对较轻时,循环的开销可能非常显著。
为了让用户更容易利用更快的“分段-元素”循环,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::string
和 window
),而不仅仅是与恢复类型相对应的元素。总的来说,恢复的类型越多,潜在的性能提升就越大。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
,编译器原则上拥有足够的信息来消除虚函数调用。在使用 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] 请注意,所有 sprite
s 都属于一个分段:这就是为什么地精 #1 和 #3 不相邻的原因。读者练习:修改示例代码,使精灵根据其具体类型进一步分段。
[12] 除非应用了小型缓冲区优化。
[13] 实际上,它对于某个内部定义的 Concept2
是 boost::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)实际上是闭合多态领域中虚函数消除的等效方法。