Search code examples
c++debuggingboostpolymorphismtype-safety

C++: Monitoring multiple value types


I want to write a class that can monitor a bunch of different values for easy debugging. Imagine setting "watches" in a visual debugger. I'm picturing something like this:

struct Foo {
    int x = 0;
    std::string s = "bar";
};

int main() {
    Foo f;
    ValueMonitor::watch("number", &f.x);
    ValueMonitor::watch("string", &f.s);
    for (int i = 0; i < 10; ++i) {
        ++f.x;
        if (i > 5) {
            f.s = "new string";
        }

        // print the current value of the variable with the given key
        // these should change as the loop goes on
        ValueMonitor::print("number");
        ValueMonitor::print("string");
        // or
        ValueMonitor::printAll();

        // obviously this would be unnecessary in this example since I
        // have easy access to f, but imagine monitoring different
        // values from all over a much larger code base
    }
}

Then these could be easily monitored somewhere in the application's GUI or whatever.

However, I don't know how to handle the different types that would be stored in this class. Ideally, I should be able to store anything that has a string representation. I have a few ideas but none of them really seem right:

  1. Store pointers to a superclass that defines a toString function or operator<<, like Java's Object. But this would require me to make wrappers for any primitives I want to monitor.
  2. Something like boost::any or boost::spirit::hold_any. I think any needs to be type casted before I can print it... I guess I could try/catch casting to a bunch of different types, but that would be slow. hold_any requires defined stream operators, which would be perfect... but I can't get it to work with pointers.

Anyone have any ideas?


Solution

  • I found a solution somewhere else. I was pretty blown away, so might as well post it here for future reference. It looks something like this:

    class Stringable
    {
    public:
        virtual ~Stringable() {};
        virtual std::string str() const = 0;
    
        using Ptr = std::shared_ptr<Stringable>;
    };
    
    template <typename T>
    class StringableRef : public Stringable
    {
    private:
        T* _ptr;
    
    public:
        StringableRef(T& ref)
            : _ptr(&ref) {}
        virtual ~StringableRef() {}
    
        virtual std::string str() const
        {
            std::ostringstream ss;
            ss << *_ptr;
            return ss.str();
        }
    };
    
    class ValueMonitor
    {
    private:
        static std::map<std::string, Stringable::Ptr> _values;
    
    public:
        ValueMonitor() {}
        ~ValueMonitor() {}
    
        template <typename T>
        static void watch(const std::string& label, T& ref)
        {
            _values[label] = std::make_shared<StringableRef<T>>(ref);
        }
    
        static void printAll()
        {
            for (const auto& valueItr : _values)
            {
                const String& name = valueItr.first;
                const std::shared_ptr<Stringable>& value = valueItr.second;
                std::cout << name << ": " << value->str() << std::endl;
            }
        }
    
        static void clear()
        {
            _values.clear();
        }
    };
    
    std::map<std::string, Stringable::Ptr> ValueMonitor::_values;
    

    .

    int main()
    {
        int i = 5;
        std::string s = "test"
        ValueMonitor::watch("number", i);
        ValueMonitor::watch("string", s);
        ValueMonitor::printAll();
    
        i = 10;
        s = "new string";
        ValueMonitor::printAll();
    
        return 0;
    }