c++oopoption-typemembers

C++ Keep tracks of changes inside a class


Imagine a class representing a mail :

class mail {
    string subject;
    string content;
    date receivedDate;
};

Now what I want to achieve is to know if my mail data is set, and once they're set, which ones where changed. I could go with a combination of std::optional and a std::map like this :

class Mail {
    std::optional<string> subject;
    std::optional<string> content;
    std::optional<date>   receivedDate;

    enum EField { Subject, Content, ReceivedDate };

    typedef std::map<EField, bool> ChangedMap;

    ChangedMap changedFields;

public:
    Mail(optional<string> subject, ... ) {
        // initialize map with fields... hard coded
    }

    bool HasSubject() const { return subject; }

    string GetSubject() const { return subject.get(); }

    void SetSubject(const std::string& newSubject) {
        subject = newSubject;
        changedFields[Subject] = true;
    }

    void RemoveSubject() {
        changedFields[Subject] = HasSubject();
        subject.reset();
    }

    bool IsSubjectChanged() const {
        return changedFields[Subject];
    }
};

But I really think I am missing something crucial here. Would you see any better way to do it, preferably with less memory usage and no hardcoded values ?

I thought about about inheriting from std::optional but I don't see it as a good thing too.

Thanks


Solution

  • Let's generalize this problem: given a type T, I want a wrapper tracked<T> that keeps track of the history of reads/writes at run-time.

    I would approach this problem by using std::tuple and metaprogramming. Firstly, let's define mail in terms of an std::tuple:

    class mail
    {
    private:
        std::tuple<string, string, date> _data;
    
    public:
        // `variant_type` could be automatically computed from the
        // tuple type.
        using variant_type = std::variant<string, string, date>; 
    
        enum class index
        {
            subject = 0,
            content = 1,
            date = 2
        };
    
        template <index TIndex>
        decltype(auto) access()
        {
            return std::get<static_cast<std::size_t>(TIndex)>(_data);
        } 
    };
    

    I would then create something like tracked<T> that keeps track of the operations executed on T:

    template <typename T>
    class tracked
    {
    private:
        using index_type = typename T::index;
        using variant_type = typename T::variant_type;
    
        struct write_action
        {
            variant_type _before;
            variant_type _after;
        };
    
        struct read_action
        {
             index_type _index;
        };
    
        T _data;
        std::vector<std::variant<write_action, read_action>> _history;
    
    public:
        template <index TIndex>
        const auto& read() const 
        {
            _history.emplace_back(read_action{TIndex});
            return _data.access<TIndex>();
        }
    
        template <index TIndex, typename T>
        void write(T&& new_value) const 
        {
            // Remember previous value.
            variant_type _before{_data.access<TIndex>()};
    
            _history.emplace_back(write_action{_before, new_value});
            return _data.access<TIndex>() = std::forward<T>(new_value);
        }
    };
    

    The code above is not completely correct, as you need constructors for the action types, exception handling, move semantics support, and much more. I hope you get the general idea though.