Louka
Louka

Reputation: 152

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

Upvotes: 1

Views: 281

Answers (1)

Vittorio Romeo
Vittorio Romeo

Reputation: 93324

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.

Upvotes: 1

Related Questions