如何 return 来自输入迭代器的高性能变体?
How to return a variant from an input iterator with high performance?
我有一些文件格式解码器,return是自定义 input iterator。此迭代器的值类型(当使用 *iter
取消引用时)可以是许多标记类型之一。
这是一个简化的用法示例:
File file {"/path/to/file"};
for (const auto& token : file) {
// do something with token
}
这个token
怎么会有多种可能的类型呢?根据令牌的类型,其有效负载的类型也会发生变化。
性能在遍历过程中很重要。例如,我不想要任何不必要的分配。这就是为什么迭代器的类型是 输入迭代器 的原因:一旦迭代器前进,根据 InputIterator
标记的要求,先前的标记就会失效。
目前我有两个想法:
使用单个 Token
class 和私有 union
所有可能的有效负载(及其 public getters ) 和 public 类型 ID (enum
) getter。
用户需要打开此类型 ID 才能知道要调用哪个 payload getter:
for (const auto& token : file) {
switch (token.type()) {
case Token::Type::APPLE:
const auto& apple = token.apple();
// ...
break;
case Token::Type::BANANA:
const auto& banana = token.banana();
// ...
break;
// ...
}
}
虽然这可能是我在 C 中会选择的,但我不喜欢 C++ 中的这种解决方案,因为用户仍然可以调用错误的 getter 并且没有任何东西可以强制执行此操作([=69 除外) =]-时间检查,我想避免性能问题)。
创建一个抽象 Token
基础 class,它有一个 accept()
方法来接受访问者,以及多个具体 classes(一个用于每个有效载荷类型)继承这个基础 class。在迭代器对象中,在创建时实例化每个具体 class 之一。还有一个 Token *token
成员。迭代时,填充适当的预分配负载对象,并设置this->token = this->specificToken
。使operator*()
returnthis->token
(参考)。要求用户在迭代期间使用访问者(或更糟,使用 dynamic_cast
):
class MyVisitor : public TokenVisitor {
public:
void visit(const AppleToken& token) override {
// ...
}
void visit(const BananaToken& token) override {
// ...
}
};
TokenVisitor visitor;
for (const auto& token : file) {
token.accept(visitor);
}
这为每个令牌引入了额外的函数调用,至少其中一个是虚拟的,但这可能不是世界末日;我对这个解决方案持开放态度。
还有其他有趣的解决方案吗?我认为 returning a boost::variant
or std::variant
与想法 #2 相同。
Although this is probably what I would choose in C, I'm not a fan of this solution in C++ because the user can still call the wrong getter and nothing can enforce this (except run-time checks which I want to avoid for performance concerns).
您可以反转方法并接受可调用对象,而不是将迭代器返回给用户。然后您可以在内部迭代容器并分派正确的类型。这样一来,用户就不会再因忽略您标记的联合所携带的信息而犯错,因为您负责考虑这些信息。
这是一个最小的工作示例来说明我的意思:
#include <vector>
#include <utility>
#include <iostream>
struct A {};
struct B {};
class C {
struct S {
enum { A_TAG, B_TAG } tag;
union { A a; B b; };
};
public:
void add(A a) {
S s;
s.a = a;
s.tag = S::A_TAG;
vec.push_back(s);
}
void add(B b) {
S s;
s.b = b;
s.tag = S::B_TAG;
vec.push_back(s);
}
template<typename F>
void iterate(F &&f) {
for(auto &&s: vec) {
if(s.tag == S::A_TAG) {
std::forward<F>(f)(s.a);
} else {
std::forward<F>(f)(s.b);
}
}
}
private:
std::vector<S> vec;
};
void f(const A &) {
std::cout << "A" << std::endl;
}
void f(const B &) {
std::cout << "B" << std::endl;
}
int main() {
C c;
c.add(A{});
c.add(B{});
c.add(A{});
c.iterate([](auto item) { f(item); });
}
在 Coliru 上查看并 运行。
我有一些文件格式解码器,return是自定义 input iterator。此迭代器的值类型(当使用 *iter
取消引用时)可以是许多标记类型之一。
这是一个简化的用法示例:
File file {"/path/to/file"};
for (const auto& token : file) {
// do something with token
}
这个token
怎么会有多种可能的类型呢?根据令牌的类型,其有效负载的类型也会发生变化。
性能在遍历过程中很重要。例如,我不想要任何不必要的分配。这就是为什么迭代器的类型是 输入迭代器 的原因:一旦迭代器前进,根据 InputIterator
标记的要求,先前的标记就会失效。
目前我有两个想法:
使用单个
Token
class 和私有union
所有可能的有效负载(及其 public getters ) 和 public 类型 ID (enum
) getter。 用户需要打开此类型 ID 才能知道要调用哪个 payload getter:for (const auto& token : file) { switch (token.type()) { case Token::Type::APPLE: const auto& apple = token.apple(); // ... break; case Token::Type::BANANA: const auto& banana = token.banana(); // ... break; // ... } }
虽然这可能是我在 C 中会选择的,但我不喜欢 C++ 中的这种解决方案,因为用户仍然可以调用错误的 getter 并且没有任何东西可以强制执行此操作([=69 除外) =]-时间检查,我想避免性能问题)。
创建一个抽象
Token
基础 class,它有一个accept()
方法来接受访问者,以及多个具体 classes(一个用于每个有效载荷类型)继承这个基础 class。在迭代器对象中,在创建时实例化每个具体 class 之一。还有一个Token *token
成员。迭代时,填充适当的预分配负载对象,并设置this->token = this->specificToken
。使operator*()
returnthis->token
(参考)。要求用户在迭代期间使用访问者(或更糟,使用dynamic_cast
):class MyVisitor : public TokenVisitor { public: void visit(const AppleToken& token) override { // ... } void visit(const BananaToken& token) override { // ... } }; TokenVisitor visitor; for (const auto& token : file) { token.accept(visitor); }
这为每个令牌引入了额外的函数调用,至少其中一个是虚拟的,但这可能不是世界末日;我对这个解决方案持开放态度。
还有其他有趣的解决方案吗?我认为 returning a boost::variant
or std::variant
与想法 #2 相同。
Although this is probably what I would choose in C, I'm not a fan of this solution in C++ because the user can still call the wrong getter and nothing can enforce this (except run-time checks which I want to avoid for performance concerns).
您可以反转方法并接受可调用对象,而不是将迭代器返回给用户。然后您可以在内部迭代容器并分派正确的类型。这样一来,用户就不会再因忽略您标记的联合所携带的信息而犯错,因为您负责考虑这些信息。
这是一个最小的工作示例来说明我的意思:
#include <vector>
#include <utility>
#include <iostream>
struct A {};
struct B {};
class C {
struct S {
enum { A_TAG, B_TAG } tag;
union { A a; B b; };
};
public:
void add(A a) {
S s;
s.a = a;
s.tag = S::A_TAG;
vec.push_back(s);
}
void add(B b) {
S s;
s.b = b;
s.tag = S::B_TAG;
vec.push_back(s);
}
template<typename F>
void iterate(F &&f) {
for(auto &&s: vec) {
if(s.tag == S::A_TAG) {
std::forward<F>(f)(s.a);
} else {
std::forward<F>(f)(s.b);
}
}
}
private:
std::vector<S> vec;
};
void f(const A &) {
std::cout << "A" << std::endl;
}
void f(const B &) {
std::cout << "B" << std::endl;
}
int main() {
C c;
c.add(A{});
c.add(B{});
c.add(A{});
c.iterate([](auto item) { f(item); });
}
在 Coliru 上查看并 运行。