Automatically registering class members to the containing object in C++

Viewed 130

This is a question about C++ design: how to avoid introducing a bug as a class changes. The question title indicates my point of view of what I want, but might be the wrong strategy.

I have a class called MyCell that I collect in another class called MyRow. Behind all of this is a database, and the row will, prior to destruction, want to persist itself to the database if any of its cells have changed.

class MyRowUser : public MyRow {
   public:
    MyRowUser() : MyRow() {}

    MyCell<int> registered_user_id;
    MyCell<int> birth_year;
    MyCell<UserGender> gender;
};

class MyTableUser : public MyTable {
    // Contains various things, including a collection
    // of MyRowUser objects.  Not relevant here, just
    // for context.
}

Now this isn't hard: I just have a function called IsDirty() in MyRowUser (or MyRow if I'm clever enough) that loops over the MyCell values and asks each one if it has changed. If any has, then the row persists.

A class of error I'd like to avoid, however, is that I add a new MyCell member to a row and forget to add it to the IsDirty() function that loops over the cells. And here I have not found a clever solution. Every registration scheme I think of so that the row knows about the cells requires actually doing the registrations explicitly. Notably, the MyCell constructors can't know about the Row (about this) in any automatic way I've thought of.

Any design suggestions on how to achieve this more robustly than my current manual iteration?

2 Answers

If you like boiler plate, I got something for you:

#include <tuple>
#include <type_traits>

// This you have to do once:

namespace detail {
template<class... Args>
constexpr void dump(Args...) {}


template<class T, auto en>
struct EnumTypePair {
    using type = T;
    static constexpr auto value = en;

    constexpr EnumTypePair() = default;

    constexpr EnumTypePair(const T&) noexcept {}

    template<class Tuple>
    static constexpr bool fits() {
        if constexpr(std::tuple_size_v<Tuple> > value) {
            if constexpr(std::is_same_v<typename std::tuple_element_t<static_cast<std::size_t>(value), Tuple>::type, type>) {
                return true;
            }
        }
        return false;
    }
};

template<class T, auto en>
static constexpr EnumTypePair<T, en> t{};

template<class Cells, typename Cells::names... Ns, class... Ts>
constexpr bool check_cells(EnumTypePair<Ts, Ns> ...pairs) {
    using types = typename Cells::types;
    using names = typename Cells::names;
    static_assert(std::tuple_size_v<types> == static_cast<std::size_t>(names::MAX));
    return (pairs.template fits<types>() && ...);
}

}

template<class Row>
struct cells;

template<class T>
struct MyCell {
    T content;

    auto clean() {};
    using type = T;
};

template<class Cells, typename Cells::names name>
using cell_type = typename std::tuple_element_t<static_cast<std::size_t>(name), typename Cells::types>;

template<class Cells, typename Cells::names name>
using content_type = typename cell_type<Cells, name>::type;


template<class ...Ts>
using cell_tuple = std::tuple<MyCell<Ts>...>;

template<class Child>
struct Row {
    using names = typename cells<Child>::names;

    template<names name> 
    auto get_cell() -> cell_type<cells<Child>, name>& {
        return std::get<static_cast<std::size_t>(name)>(m_cells);
    }

    template<names name> 
    auto get() -> content_type<cells<Child>, name>& {
        return std::get<static_cast<std::size_t>(name)>(m_cells).content;
    }

    
    template<std::size_t ... Ns>
    void clean_impl(std::integer_sequence<std::size_t, Ns...>) {
        detail::dump( get_cell<names(Ns)>()... );
    }

    auto make_clean() {
        clean_impl(std::make_integer_sequence<std::size_t, names::MAX>{});
    }

    typename cells<Child>::types m_cells;
};

// This you have to do for each row:

struct MyRow;

template<>
struct cells<MyRow> {
    enum names {
        id,
        birth_year,
        gender,
        MAX
    };
    
    using types = cell_tuple<int, int, char>;

    // To check against reordering
    static_assert(detail::check_cells<cells<MyRow>>(detail::t<int, id>, detail::t<int, birth_year>, detail::t<char, gender>));
};

struct MyRow: Row<MyRow> {
};


int main() {
    MyRow r;

    // This is sadly very verbose, maybe you have to write getters for every field.
    r.get<cells<MyRow>::id>() = 1;
}

It should be very hard to make an error but you also have to type a lot....

EDIT: By the power of fold expressions, I got rid of a lot of annoying stuff. And now it is impossible to forget the cleaning up. It needs a bit of boilerplate to protect you from reordering the elements and then having a bad day.

EDIT 2: I just saw, that you want to use C++14. This will make the order checking more annoying. The automatic clean up is no problem, I added a workaround for the pack expansion. And I fixed a problem with the integer_sequence.

If you can guarantee that you will never use Cells outside of a common MyRow class and that Cell cannot contain MyRows again, you could do the following:

class MyRow {
  public:
    static MyRow* g_current_row;
    MyRow() : dirty(false) { g_current_row = this; }
    void makeDirty() { dirty = true; }
    void clearDirty() { dirty = false; }
    bool isDirty() const { return dirty; }

  private:
    bool dirty;
};

and use it in the Cell constructor:

Cell::Cell() : owner(MyRow::g_current_row) {}

void Cell::update(T value) { owner->makeDirty(); }

The MyRowUser constructor will call MyRow::MyRow, which sets g_current_row for the Cell constructors after it.

Related