Kirk KD
Kirk KD

Reputation: 136

C++ How to override class field with a different type (without template)?

So I am trying to write a simple interpreter in c++, but ran into some problems. I have a Token class, which holds an enum TokenType, and a TokenValue object. The TokenValue class is the base class of several other classes (TV_String, TV_Int, and TV_Float).

Here is the code for the TokenValue and its children classes:

// TokenValue.h

class TokenValue
{
public:
    void* value = NULL;

    virtual bool operator ==(const TokenValue& tv) const
    {
        return typeid(this) == typeid(tv) && value == tv.value;
    }
};

class TV_Empty : public TokenValue {};

class TV_String : public TokenValue
{
public:
    std::string value;

    TV_String(std::string value); // The constructors just assign the value argument to the value field
};

class TV_Int : public TokenValue
{
public:
    int value;

    TV_Int(int value);
};

class TV_Float : public TokenValue
{
public:
    float value;

    TV_Float(float value);
};

Here's the code for Token:

// Token.h

class Token
{
public:
    enum class TokenType
    {
        // all the different types
    }

    TokenType type;
    TokenValue value;

    Token(TokenType type, TokenValue value); // just initialises type and value, nothing else
}

The problem I am having is that the value field is not being changed when I use any of the children classes (it always shows 00000000 when I print it, I assume that's the value of void* value = NULL, but not sure). From research I think it could be solved by using templates, but in my case I can't use templates because Token never know the type of its corresponding TokenValue.

So how can I override the type and value of the value field and access the correct value in the children classes, and in the == operator?

(Thanks to Jarod42 I realised it doesn't "override" the field, it creates a new field with a different type and the same name.)

Upvotes: 1

Views: 231

Answers (2)

Pete Becker
Pete Becker

Reputation: 76370

This is a somewhat nasty situation, but it can be handled with a bit of indirection:

struct TokenValue {
    virtual bool equals(const TokenValue&) const = 0;
};

bool operator==(const TokenValue& lhs, const TokenValue& rhs) {
    return typeid(lhs) == typeid(rhs) && lhs.equals(rhs);
}

Now, derived classes can have their own value field (if that's appropriate), and override equals, knowing that the argument will always be their own type:

struct TV_empty : TokenValue {
    bool equals(const TokenValue&) const { return true; }
};

struct TV_string : TokenValue {
    std::string value;
    bool equals(const TokenValue& other) const {
        return value == static_cast<TV_string&>(other).value;
    }
}

and so on.

Yes, if you're paranoid, you could use dynamic_cast inside the equals functions.

Upvotes: 1

Remy Lebeau
Remy Lebeau

Reputation: 596673

What you are attempting to do will not work, because TokenValue is a base class and you are storing it by value in Token, so if you attempt to assign a TV_String object, a TV_Int object, etc to Token::value, you will slice that object, losing all info about the derived class type and its data fields.

To work with polymorphic classes correctly, you will need to make the Token::value field be a pointer to a TokenValue object instead, eg:

class TokenValue
{
public:
    virtual ~TokenValue() = default;

    virtual bool equals(const TokenValue*) const = 0;

    bool operator==(const TokenValue &rhs) const {
        return equals(&rhs);
    }
};

class TV_Empty : public TokenValue {
public:
    bool equals(const TokenValue* tv) const override {
        return (dynamic_cast<const TV_Empty*>(tv) != nullptr);
    }
};

class TV_String : public TokenValue
{
public:
    std::string value;

    TV_String(const std::string &value) : value(value) {}

    bool equals(const TokenValue* tv) const override {
        TV_String *s = dynamic_cast<const TV_String*>(tv);
        return (s) && (s->value == value);
    }
};

class TV_Int : public TokenValue
{
public:
    int value;

    TV_Int(int value) : value(value) {}

    bool equals(const TokenValue* tv) const override {
        TV_Int *i = dynamic_cast<const TV_Int*>(tv);
        return (i) && (i->value == value);
    }
};

class TV_Float : public TokenValue
{
public:
    float value;

    TV_Float(float value) : value(value) {}

    bool equals(const TokenValue* tv) const override {
        TV_Float *f = dynamic_cast<const TV_Float*>(tv);
        return (f) && (f->value == value);
    }
};

...
struct EmptyToken {};

class Token
{
public:
    enum class TokenType
    {
        Empty,
        String,
        Int,
        Float
        ...;
    };

    TokenType type;
    std::unique_ptr<TokenValue> value;

    static TokenType GetTokenType(const TokenValue *tv) {
        if (dynamic_cast<TV_Empty*>(tv) != nullptr)
            return TokenType::Empty;
        if (dynamic_cast<TV_String*>(tv) != nullptr)
            return TokenType::String;
        if (dynamic_cast<TV_Int*>(tv) != nullptr)
            return TokenType::Int;
        if (dynamic_cast<TV_Float*>(tv) != nullptr)
            return TokenType::Float;
        return ...;
    }

    Token(std::unique_ptr<TokenValue> value) : Token(GetTokenType(value.get()), std::move(value)) {}

    Token(TokenType type, std::unique_ptr<TokenValue> value) : type(type), value(std::move(value)) {}

    explicit Token(const EmptyToken &) : type(TokenValue::Empty), value(std::make_unique<TV_Empty>()) {}
    explicit Token(const std::string &value) : type(TokenValue::String), value(std::make_unique<TV_String>(value)) {}
    explicit Token(int value) : type(TokenValue::Int), value(std::make_unique<TV_Int>(value)) {}
    explicit Token(float value) : type(TokenValue::Float), value(std::make_unique<TV_Float>(value)) {}
    ...
};
Token tk1(std::string("test"));
Token tk2(12345);
if (*(tk1.value) == *(tk2.value)) ...
if (tk1.value->equals(tk2.value.get())) ...
...

However, what you are essentially doing is replicating what std::variant already is (a tagged union), so you should get rid of TokenValue completely and just use std::variant instead, eg:

struct EmptyToken {};

class Token
{
public:
    enum class TokenType
    {
        Empty,
        String,
        Int,
        Float
        ...;
    };

    std::variant<EmptyToken, std::string, int, float, ...> value;

    explicit Token(const EmptyToken &value) : value(value) {}
    explicit Token(const std::string &value) : value(value) {}
    explicit Token(int value) : value(value) {}
    explicit Token(float value) : value(value) {}
    ...

    TokenType GetTokenType() const
    {
        static const TokenType types[] = {TokenType::Empty, TokenType::String, TokenType::Int, TokenType::Float, ...};
        return types[value.index()];
    };

    ...
};
Token tk1(std::string("test"));
Token tk2(12345);
if (tk1.value == tk2.value) ...
...

Upvotes: 1

Related Questions