Skip to content
Learn Netverks
1

How to overload [ ] operator when using pointers to polymorphic objects?

asked 4 hours ago by @qa-lv6nvtt7ngmg00qem02d 0 rep · 182 views

c++ pointers polymorphism operator overloading

I'm making a JSON parser. My goal is to try and implement it so I can chain accessing keys like so:

std::string errorMsgOutput;
std::optional<double> myNumber = myJSONObj["topKey"]["subkey"].getNumber(errorMsgOutput);

The base of my class hierarchy is called JSONBase. The problem is, just using JSONBase to store data and return types means I won't be able to use polymorphic overridden methods which require using JSONBase*. However I have no idea how I'd write an overloaded [] operator to both return JSONBase* and also work with chaining (or better yet, using smart pointers).

How would I make this work?

My current setup looks something like this:

class JSONBase {
    protected:
        std::optional<std::string> mErr = std::nullopt;

    public:
        std::optional<double> getNumber(std::string& errMsg) {
            errMsg = "Error attempting to retrieve number from non-number JSON object";
            return std::nullopt;
        }
        
        JSONBase& operator [] (const std::string& key) {
            this->mErr = "Error attempting to retrieve indexed value from non-array JSON object";
            return this;
        }
}

//Stores JSON <key, value> objects
class Object: public JSONBase {
    private:
        std::unordered_map<std::string, JSONBase> mData;
        
    public:
        JSONBase& operator [] (const std::string& key) {
            if (this->mErr) {
                return this;
            }

            if (!this->mData.contains(key)) {
                this->mErr = "JSON dictionary does not contain key \"" + key + '\"';
                return this;
            }

            return this->mData[key];
        }
}

//Stores JSON number data
class Number: public JSONBase {
    private:
        double mData;
        
    public:
        std:optional<double> getNumber(std::string& errMsg) {
            if (this->mErr) {
                errMsg = *this->mErr;
                return std::nullopt;
            }
            
            return this->mData;
        }
}

This is simplified and leaves out the Array, Boolean, and String classes, but I plan to implement those in the exact same way.

Comments on this question (0)

Use comments to ask for clarification — answers go in the answer box below.

Log in to comment on this question.

3 answers

6

Accepted answer

Store the values as owning pointers, but keep operator[] returning JSONBase&. That keeps the syntax myJSONObj["topKey"]["subkey"].getNumber(errorMsgOutput) because the result of each operator[] is still an object reference, not a pointer.

mData cannot be std::unordered_map<std::string, JSONBase> for this design: that stores JSONBase objects by value, so derived Object, Number, etc. are sliced. Make the operations that vary by JSON type virtual, and return *this, not this, when returning a reference.

class JSONBase {
protected:
    std::optional<std::string> mErr = std::nullopt;

public:
    virtual ~JSONBase() = default;

    virtual std::optional<double> getNumber(std::string& errMsg) {
        if (mErr) {
            errMsg = *mErr;
            return std::nullopt;
        }

        errMsg = "Error attempting to retrieve number from non-number JSON object";
        return std::nullopt;
    }

    virtual JSONBase& operator[](const std::string& key) {
        mErr = "Error attempting to retrieve indexed value from non-array JSON object";
        return *this;
    }
};

class Object : public JSONBase {
private:
    std::unordered_map<std::string, std::unique_ptr<JSONBase>> mData;

public:
    JSONBase& operator[](const std::string& key) override {
        if (mErr) {
            return *this;
        }

        auto it = mData.find(key);
        if (it == mData.end()) {
            mErr = "JSON dictionary does not contain key \"" + key + "\"";
            return *this;
        }

        return *it->second;
    }
};

class Number : public JSONBase {
private:
    double mData;

public:
    explicit Number(double value) : mData(value) {}

    std::optional<double> getNumber(std::string& errMsg) override {
        if (mErr) {
            errMsg = *mErr;
            return std::nullopt;
        }

        return mData;
    }
};

Returning JSONBase* would change the call syntax, because the next [] would be applied to a pointer. Returning JSONBase& gives both chaining and virtual dispatch.

Morgan Brooks · 0 rep · 4 hours ago

2

If we assume that if the key was found in the map there exists a non-null JSONBase you can simply dereference mData when returning it. Your insert method (or whatever method you use for inserting JSON data) should allocate the memory for the value in the key-value pair.

class Object : public JSONBase {
private:
    std::unordered_map<std::string, JSONBase*> mData;
public:
    JSONBase& operator[](const std::string& key) {
        ...
        return *this->mData[key];
    }
}

Of course, it's recommended that you use smart pointers instead.

Another issue you have is that the member functions of the base class should be virtual to allow dynamic dispatch to choose the overriden member functions of the derived classes.

Jamie Morris · 0 rep · 4 hours ago

2

The class design is not good enough, because derived classes hide the base members, and a user has to cast pointers.

You might want something like

class JSON {
 public:
  class Item;
  using List = std::vector<Item>;

 private:
  List items_;
};

class JSON::Item {
 private:
  std::optional<std::string> key_;
  std::variant<std::nullptr_t, bool, int64_t, double, std::string, List, JSON> value_;
};

Morgan Price · 0 rep · 4 hours ago

Your answer