Overall design: I have an aggregate class C
that contains N
member variables of type M_i, i = 1 ... N
that each have a common write-only update()
interface as well as class-specific read-only accessor functions [F]un_i(), [F] = any letter, i = 1 .. N
(they do not have such regular names in reality). Each of the member types M_i
forms an independent abstraction of its own, and is used elsewhere in my program.
The aggregate class needs to update all the members in a single transaction, so it has an update()
function of its own calling the update()
member function of all its member variables.
// building blocks M_i, i = 1 ... N
class M_1
{
public:
// common write-only interface
void update();
// M_1 specific read-only interface
int fun_1() const;
// ...
int fun_K() const;
private:
// impl
};
// ...
class M_N
{
public:
// common write-only interface
void update();
// M_N specific read-only interface
int gun_1() const;
// ...
int gun_K() const;
private:
// impl
};
// aggregate containing member variables M_i, i = 1 ... N
class C
{
public:
// update all members in a single transaction
void update()
{
m1_.update();
// ...
mN_.update();
}
// read-only interface?? see below
private:
M_1 m1_;
// ...
M_N mN_;
};
Question: the do I access the various member functions of the various member variables in the aggregate class? I can think of three alternatives:
Alternative 1: write N * K
delegates to all K
member functions of all N
member variables
class C
{
int fun_1() const { return m1_.fun_1(); }
// ...
int fun_K() const { return m1_.fun_K(); }
// ...
int gun_1() const { return mN_.gun_1(); }
// ...
int gun_K() const { return mN_.gun_K(); }
// as before
};
int res = C.fun_5(); // call 5th member function of 1st member variable
Alternative 2: write N
accessors to all N
member variables
class C
{
M_1 const& m1() const { return m1_; }
// ...
M_N const& mN() const { return mN_; }
// as before
};
int res = C.m1().fun_5(); // call 5th member function of 1st member variable
Alternative 3: write 1
accessor template to all N
member variables
class C
{
public:
enum { m1, /* ... */ mN };
template<std::size_t I>
auto get() const -> decltype(std::get<I>(data_))
{
return std::get<I>(data_);
}
private:
std::tuple<M_1, /* ... */ M_N> data_;
};
int res = C.get<m1>().fun_5(); // call 5th member function of 1st member variable
Alternative 1 avoids violating the Law of Demeter but it needs an awful lot of tedious boiler plate code (in my application, N = 5
and K = 3
, so 15
delegating wrappers). Alternative 2 cuts down on the number of wrappers, but the calling code feels a little uglier to me. But since all that code is read-only, and modfications can only happen through the consistent aggregate update()
, my current opinion that Alternative 2 is preferable to Alternative 1 (and at least safe). If that's the case, then a fortiori, Alternative 3 should be the best choice since it uses only a single accessor and has the same safety guarantees as Alternative 2.
Question: what is the preferred interface for this type of code?
Turning my comment into an answer.
If you decide to go with alternative 1 (N*K delegates), you can use Boost.Preprocessor to do the boilerplate work for you:
Translated into pseudo-code to better highlight the working logic:
One other possibility is
Though you need to decide if this makes sense for you. You would need to write a huge nested switch inside though, but the interface is simpler.
This method is ideal of course if you can store your objects in an array, and all member functions are part of a common interface of
M_i
types.The solution that gives you the best user friendly code with compile time resolution of calls has to rely on templates.
Indeed, if you want to be able to call
fun(i,j)
(actuallyfun<i,j>()
) wherei
is an index to a member variable, andj
an index to a member function of this variable, then you have to define the mappings. Both mappings.First mapping between the member variable index and the variable itself, that implies a mapping between the member variable index and the variable type.
Second mapping between the member function index and the member function itself. However, as this mapping depend on the type of the indexed member variable, it has to be defined for every combination. You cannot provide the user a completely indexed solution without defining this mapping. Or the other way round: if you don't want the caller to bother about the type of the i-th variable to know what is the name of the j-th function he wants to call (that depends on the type of the i-th variable), then you have to provide the mappings.
With this, the user will be able to call
int v = c.fun<i, j>()
without the knowledge of neither the type of the i-th variable, neither the name of the j-th function for this i-th variable....
Now, if you want that the user can make calls with
i
andj
as run-time variables, then this is not the way to go. Prefer anint fun(i, j)
function with lots ofif
andswitch
. You cannot have both.I would completely separate the update behaviour from the single element functionalities. All the M_i classes should implement an Updatable interface that simply contains the update method.
This allows you to safely expose N accessors to (non const) Updatable interfaces.
Given the aggregate class C you can then:
expose N accessor to the const M_i classes
ask for the Updatable interface of a given M_i class. By accessing this (non-const) reference you can safely issue updates to any of the M_i instances.
.