在C#中,"yield return"关键字为迭代集合提供了一种简洁的方式。本文将展示如何在原生C++中创建类似的迭代技术,而无需复杂的代码。
"yield return"迭代器是一个为简单性而创建的语言特性。通常,迭代整个集合并存储所有需要的上下文在局部变量中,比创建一个复杂的自定义迭代器对象要简单得多,后者需要在随后的检索操作中存储其状态。
下面是一个使用"yield return"迭代器的示例代码:
class Program {
static void Main(string[] args) {
foreach (int i in GetRandom())
Console.WriteLine(i);
}
public static IEnumerable<int> GetRandom() {
for (int i = 0; i < 5; i++)
yield return Random.Get();
}
}
这个程序实现了一个包含五个随机数的集合。然而,迭代函数似乎永远不会返回,或者可能返回五次!
当然,C++没有"foreach"关键字,但考虑以下代码:
SomeCollection C;
Enumerator<SomeCollection, int> E(C);
for (int i; E.Get(i); )
cout << i << endl;
它看起来并不比C#的代码更复杂。
"SomeCollection"的唯一前提是有一个返回整数的"Enumerate"方法。此外,可以使用以下形式的"Enumerate"函数:
int SomeCollection::Enumerate() {
for (int i = 0; i < 4; i++)
yield_return(rand());
return rand();
}
简单的返回表明已经完成了迭代(实际上,在C#中也应该是这样),而"yield_return"设置了中间结果。
本文中的代码将允许编写一个与上面看到的程序完全相同的程序。不需要额外的官僚程序,不需要长模板名称,也不需要任何其他不便。嗯,有一种不同性质的不便——代码将不可避免地包含<windows.h>,除非应用了一些包装器,并将系统依赖的代码移动到一个专用的编译单元。
Windows中一个非常强大但被低估的特性是fibers(可能是因为在基于UNIX的系统中找不到这样的特性)。
Fiber只是一个线程的上下文。它包括一个单独的栈,fiber-local-storage和一个在切换时存储寄存器值的地方。Fiber切换发生在用户模式下,因此它们的开销非常低。最后但同样重要的是,fibers由用户调度!这意味着fiber可以被用作调度延迟过程调用的非常强大的工具,这正是需要的。
创建一个fiber只需要调用CreateFiber,提供一些上下文信息。它不会返回一个句柄,而是返回一个用户指针到fiber,线程可以显式地切换到它。每个线程最初都不是一个fiber,所以必须调用ConvertThreadToFiber来创建第一个fiber,并设置fiber自定义数据(由ConvertThreadToFiber的调用者提供的任意指针值)。
切换是微不足道的,它只是一个调用SwitchToFiber(PVOID fiber)。
基本上,要构建一个如上所述工作的程序,需要实现两件事:"yield_return"函数和"Enumerator"类的"Get()"函数。"Enumerator"类的头文件如下:
template<class T> class Enumerator {
public:
Enumerator(T &owner) : Owner(&owner), Fiber(0), ToolFiber(0) {}
Enumerator(T *owner) : Owner(owner), Fiber(0), ToolFiber(0) {}
~Enumerator();
bool Get(R &result); // 核心函数
private:
static void Return(const R &value); // 存储yield_return的结果的函数
template<class R> friend void yield_return(const R &result); // yield_return本身
virtual void *ValidatePointer(void *_ptr) // 虚拟只是为了强制评估
{
return *(void **)_ptr;
}
static void Enumerate(Enumerator *This); // 使C++中的成员指针变得舒适一些
void Prepare(); // 准备步骤
R *pResult; // 结果,设置为Get中的&result
T *Owner; // 迭代的对象
void *Fiber, *ToolFiber; // fibers
};
让看看一个相当无聊的准备步骤:
template<class T, class R> void Enumerator<T, R>::Prepare() {
if (!Fiber) {
Fiber = GetCurrentFiber();
__try {
ValidatePointer(Fiber);
} __except (EXCEPTION_EXECUTE_HANDLER) {
Fiber = ConvertThreadToFiber(this);
}
}
if (!ToolFiber) // 枚举fiber在这里!
ToolFiber = CreateFiber(4096, (LPFIBER_START_ROUTINE)Enumerate, this);
}
枚举函数:
template<class T, class R> void Enumerator<T, R>::Enumerate(Enumerator *This) {
void *Fiber = This->Fiber; // 存储fiber - 它可能会改变
*This->pResult = This->Owner->Enumerate();
This->Owner = NULL; // 表示集合结束
SwitchToFiber(Fiber); // 返回到原始fiber
}
这个函数是从ToolFiber上下文中调用的,所以SwitchToFiber(Fiber)应该在那里;如果它在返回之前没有切换回来,线程将终止!
全能的Get:
template<class T, class R> bool Enumerator<T, R>::Get(R &result) {
if (!Owner) // 没有要迭代的对象 - 从来没有一个
// 或迭代已经结束
return false;
pResult = &result; // 设置结果指针
Prepare(); // 准备
if (ToolFiber) // 准备好了
{
SwitchToFiber(ToolFiber); // 让开始(或继续)
// 枚举!
if (!Owner) // fiber的方式表示迭代结束
{
DeleteFiber(ToolFiber); // 删除工具fiber
ToolFiber = Fiber = 0; // 清理
}
}
return true; // 成功!
}
设置结果:
template<class T, class R> void Enumerator<T, R>::Return(const R &value) {
Enumerator *E = (Enumerator *)GetFiberData(); // 获取上下文,
// 存储在fiber数据中
if (E->Fiber != GetCurrentFiber()) // 检查,如果是同一个fiber -
// Fiber字段在清理时被欺骗
{
*E->pResult = value; // 设置结果
SwitchToFiber(E->Fiber); // 返回到主fiber
}
}
现在有了模拟返回 - 它由Return函数中的SwitchToFiber(E->Fiber)处理 - 它将控制权转移回原始fiber,可以使用pResult读取结果。
一切都进行得很顺利,很干净 - fiber已经结束,一切都很美好。但是如果放弃了迭代器呢?
唯一的解决方案是将控制权设置回ToolFiber,并迭代直到集合结束,可能明确地告诉迭代程序它的结果将被忽略,所以它可以直接返回任何东西。在解决方案中,枚举器只是被告知要迭代。然而,Fiber字段被设置为ToolFiber,所以没有发生fiber切换,结果也没有被复制。这可以提高这些最终迭代的性能。
析构函数:
template<class T, class R> Enumerator<T, R>::~Enumerator() {
if (ToolFiber) {
Fiber = ToolFiber; // 欺骗fiber指针
SwitchToFiber(ToolFiber); // 切换到枚举器
DeleteFiber(ToolFiber); // 删除枚举器
ToolFiber = Fiber = 0;
}
}
现在唯一剩下的就是使返回中间结果的方式更加文明。调用Enumerator<Type, Type>::Return(value)比yield_result(value)要复杂得多。所以让让事情变得简单:
template<class R> inline void yield_return(const R &result) {
Enumerator<int, R>::Return(result); // 集合类型在这里不需要,所以放'int'
}
无论在哪里可以找到使用伪迭代对象或迭代器本身的复杂性,这种方法都很合适。与经典迭代器相比,它有一些开销,但它的真正力量在于它的简单性。如果在软件中应用这段代码,请让知道 - 将很高兴帮助某人。
有一个想法,每个值都应该由yield return返回,而迭代函数本身返回void。这实际上是返回空集合的唯一方式,会使代码更加统一,并简化集合结束的检测。