В более крупном проекте мне нужен (довольно простой) анализатор выражений, способный принимать числовые значения, операторы и строковые идентификаторы. Хорошо, лексический синтаксический анализатор с вводом, который дает жетоны по одному синтаксическому синтаксическому анализатору. Поскольку токен может содержать только один тип значения за раз, я решил использовать объединение. Проблемы возникли вскоре, когда я понял, что идентификатор — это произвольная строка неизвестной длины, и что std::string
внутри профсоюзов было не самое простое дело …
Общий план состоит в том, чтобы инициализировать лексический синтаксический анализатор с некоторыми входными данными, и синтаксический анализатор выражений неоднократно вызывает его следующий участник, чтобы получать по одному токену за раз. Две мои первые попытки разработать класс Token закончились ужасно сложным кодом для первого и кода, вызывающего UB для второго.
Прежде чем идти дальше, я хотел бы убедиться, что мой текущий Token
class может быть использован для создания над ним полной техники. В своих тестах я могу успешно создавать токены всех типов, копировать их или хранить в стеках или векторах и получать доступ к их типам и значениям, но я также знаю, что оно работает ни то, ни другое это правильно по стандарту и портативному, ни не содержит антипаттернов… Что касается версий C ++, я ожидаю, что буду следовать стандартам C ++ 14 и выше.
/**
* Token represents a token extracted by a lexical parser.
*
* It has a type among integer, double, operator (single char) or string and
* contains an appropriate value, except for the special type Eof which has
* no value and represents the end of the input data
*
* It is a copyable and default constructible type (default constructor gives
* an Eof token) or can be constructed from a value of an acceptable type to
* produce a token of that type.
*/
class Token {
public:
enum class Type { Int, Double, Operator, Identifier, Eof } type;
protected:
// only 1! member at at time => union
union Foo {
// group trivial member to be able to process them as a whole
// because Bar is a trival union
union Bar {
int val;
double fval;
char op;
} y;
// one non trivial member: shall define all special methods
Foo() { y.val = 0; }
Foo(int i) { y.val = i; }
Foo(double d) { y.fval = d; }
Foo(char c) { y.op = c; }
Foo(const std::string& str) : str(str) {};
~Foo() {}
std::string str;
} x;
public:
// Simple ctors from nothing (Eof) or an acceptable type
Token() : type(Type::Eof) {}
Token(int i) : type(Type::Int), x(i) {};
Token(double d) : type(Type::Double), x(d) {};
Token(char c) : type(Type::Operator), x(c) {};
Token(const std::string& str) : type(Type::Identifier), x(str) {};
//Copy ctor handles specifically the string member
Token(const Token& other): type(other.type) {
if (type == Type::Identifier) {
// in place construction for the string
new (&x.str) std::string(other.x.str);
}
else {
x.y = other.x.y; // magic of the trivial member y
}
}
// Explicit dtor destroys a possible string member
~Token() {
if (type == Type::Identifier) {
x.str.~basic_string();
}
}
// assignment operator again handles the string member
Token& operator = (const Token& other) {
if (type == Type::Identifier) {
if (other.type == Type::Identifier) {
x.str = other.x.str;
}
else {
// different types: we can safely destroy the destination
x.str.~basic_string();
x.y = other.x.y;
}
}
else {
if (other.type == Type::Identifier) {
// we shall construct a new string member
new (&x.str) std::string(other.x.str);
}
else {
x.y = other.x.y;
}
}
type = other.type;
return *this;
}
// const accessors...
int getVal() const { return x.y.val; }
double getFval() const { return x.y.fval; }
char getOp() const { return x.y.op; }
std::string getStr() const { return x.str; }
Type getType() const { return type; }
};
// and a stream injector to ease debugging traces
std::ostream& operator << (std::ostream& out, const Token& tok) {
switch (tok.getType()) {
case Token::Type::Int:
out << tok.getVal();
break;
case Token::Type::Double:
out << tok.getFval();
break;
case Token::Type::Operator:
out << tok.getOp();
break;
case Token::Type::Identifier:
out << tok.getStr();
break;
case Token::Type::Eof:
out << "__EOF__";
}
return out;
}
1 ответ
Для того, чтобы делать то, что вы хотите, вы можете либо иметь (не объединенные) члены разных типов, и только один будет заполнен, что и является вашим подходом;
или вы можете использовать буфер массива байтов, достаточно большой для хранения любого из различных типов, и на месте создавать и уничтожать там фактический объект. Вот как Boost’s variant
был реализован до того, как вы могли помещать такие типы в примитив union
.
Я предлагаю найти готовую зрелую версию variant
для включения в свой проект, даже если вы не используете Boost или не включаете все библиотеки Boost. Делать это хорошо сложно, и это уже было сделано другими.
Кроме того, рассматривали ли вы возможность использования string_view
вместо string
? Вам не нужно копировать токен, если вы можете ссылаться на него в исходном вводе. Это позволит избежать проблем, с которыми вы сталкиваетесь.
(Опять же, если у вас нет std::string_view
, получите автономную реализацию для включения в свой проект. Помните, что все эти причудливые новые типы библиотек были проверены и хорошо изношены, прежде чем быть включены в стандарт ISO!)