The problem is as following: we have a database of many different values. Many different classes can modify values in it, and we have to emit an event on any modification, so that other classes can be notified about modification. We currently have wrappers like this (see it online):
struct Database {
std::string s;
int i;
};
template <auto PMD>
struct DbFieldAccessor {
// template magic, from (PMD = T C::*) extracts T
using FieldType = PmdFieldType_t<decltype(PMD)>;
explicit DbFieldAccessor(Database& database) : m_database{&database} {}
constexpr auto operator->() noexcept -> FieldType* {
m_modified = true;
return &(m_database->*PMD);
}
auto isFieldModified() const noexcept -> bool { return m_modified; }
private:
Database* m_database;
bool m_modified = false;
};
template <class Db>
class DatabaseAdapter {
public:
explicit DatabaseAdapter(Db db) : m_database{std::move(db)} {}
template <auto PMD>
[[nodiscard]] auto mutableAccess() -> DbFieldAccessor<PMD> {
return DbFieldAccessor<PMD>{m_database};
}
template <auto... PMDs>
auto emitUpdates(const DbFieldAccessor<PMDs>&... accessors) -> void {
(
[](const auto& accessor) {
if (accessor.isFieldModified()) {
// emitEvent(events::FieldUpdated<PMDs>{});
}
}(accessors),
...);
}
private:
Db m_database{};
};
The expected usage is like this:
Database internalDb; // never accessed directly
DatabaseAdapter<Database> db{internalDb};
auto access = db.mutableAccess<&Database::s>(); // get access to s in Database
access->push_back('D'); // modify it
db.emitUpdates(access); // emit updates
The problem is that it's easy to forget to call emitUpdates after modifying a field. We've been trying to find a solution that calls it automagically, but couldn't come with anything:
- RAII - calling
emitUpdatesin~DbFieldAccessorcannot be done,emitUpdatescan throw - wrapping it in a function - creates unnecessary copies and is sometimes inconvenient (see above example - instead of
push_back, we need to passgetFromDb(s) + 'D')
template<auto PMD, class Db>
void modifyDb(DatabaseAdapter<Db>& db, const PmdFieldType_t<decltype(PMD)>& data)
{
auto access = db.template mutableAccess<PMD>();
*access = data;
db.emitUpdates(dispatcher, entityId, access);
}
- pass a lambda to modify database member - there's some boilerplate at call site and if we lazily pass
auto&as lambda argument, we lose any code completion features
template<auto PMD, class Db, class Func>
void modifyDb(DatabaseAdapter<Db>& db, Func&& func)
{
auto access = db.template mutableAccess<PMD>();
std::forward<Func>(func)(*access);
db.emitUpdates(dispatcher, entityId, access);
}
// at call site
modifyDb<&Database::s>(db, [](auto& s){s.push_back('D');});
Is there anything else that could be used here? All the code belongs to us, so no problem with any modifications.
If you can handle the exception that is potentially thrown by
emitUpdatesin the code that callsemitUpdatesthen you can still make use of RAII:Live Demo
As pointed out in comments, if you need the exception to travel further up the call stack before it can be handled then the above is not viable.