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
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.