I have some file format decoder which returns a custom input iterator. The value type of this iterator (when dereferencing it with *iter
) can be one of many token types.
Here's a simplified usage example:
File file {"/path/to/file"};
for (const auto& token : file) {
// do something with token
}
How can this token
have multiple possible types? Depending on the type of the token, the type of its payload changes too.
Performance is important here during traversal. I don't want any unnecessary allocation, for example. This is why the iterator's type is an input iterator: as soon as you advance the iterator, the previous token is invalidated as per the requirements of the InputIterator
tag.
I have two ideas in mind so far:
Use a single
Token
class with a privateunion
of all the possible payloads (with their public getters) and a public type ID (enum
) getter. The user needs to switch on this type ID to know which payload getter to call: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; // ... } }
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).
Create an abstract
Token
base class which has anaccept()
method to accept a visitor, and multiple concrete classes (one for each payload type) inheriting this base class. In the iterator object, instantiate one of each concrete class at creation time. Also have aToken *token
member. When iterating, fill the appropriate pre-allocated payload object, and setthis->token = this->specificToken
. Makeoperator*()
returnthis->token
(reference to). Ask the user to use a visitor during the iteration (or worse, usedynamic_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); }
This introduces additional function calls for each token, at least one of them which is virtual, but this might not be the end of the world; I remain open to this solution.
Is there any other interesting solution? I consider that returning a boost::variant
or std::variant
is the same as idea #2.