How to write read-only accessor functions in an aggregate root class?
Asked Answered
A

4

0

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?

Accelerometer answered 18/2, 2013 at 9:6 Comment(4)
Just a hint regarding boilerplate code: try Boost.Preprocessor.Nilsanilsen
@Angew I have used Boost.Preprocessor in the past for array initialization. Could you tell me which parts of that library can be used to write forwarding functions?Accelerometer
You could work with SEQ_FOR_EACH and LOCAL_MACRO, for example. I created a small sample.Nilsanilsen
@Angew Great stuff. If you add an answer, I'll upvote, even if I would never dare try use that for fear of not being able to adapt it :-)Accelerometer
N
1

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:

#include <boost/preprocessor.hpp>

// Define identifier names

#define FUNCTIONS (fun)(gun)(hun)

#define MEMBER_NAMES (m1_)(m2_)(m3_)

#define SUFFIXES (_1)(_2)(_3)


// Utility "data structure"
// Used to hand down state from iteration over functions to iteration over suffixes

#define WRAP_DATA(function, member) \
  (2, (function, member))

#define UNWRAP_DATA_FUNTION(data) \
  BOOST_PP_ARRAY_ELEM(0, data)

#define UNWRAP_DATA_MEMBER(data) \
  BOOST_PP_ARRAY_ELEM(1, data)


// Accessor-generating functionality

  // Convenience macro for generating the correct accessor name
#define CREATE_FUNCTION_NAME(data, suffix) \
  BOOST_PP_CAT(UNWRAP_DATA_FUNCTION(data), suffix)

  // Macro generating one accessor delegation
#define GENERATE_ACCESSOR(r, data, suffix) \
  int CREATE_FUNCTION_NAME(data, suffix) () const { return UNWRAP_DATA_MEMBER(data).CREATE_FUNCTION_NAME(data, suffix) (); }


// Generate accessors

class C
{

  // Execute GENERATE_ACCESSOR once for each element of SUFFIXES
#define BOOST_PP_LOCAL_MACRO(iter) \
  BOOST_PP_SEQ_FOR_EACH(GENERATE_ACCESSOR, WRAP_DATA(BOOST_PP_SEQ_ELEM(iter, FUNCTIONS), BOOST_PP_SEQ_ELEM(iter, MEMBER_NAMES)), SUFFIXES)

#define BOOST_PP_LOCAL_LIMITS (0, BOOST_PP_SEQ_SIZE(FUNCTIONS) - 1)

  // Execute BOOST_PP_LOCAL_MACRO once for each value within BOOST_PP_LOCAL_LIMITS
#include BOOST_PP_LOCAL_ITERATE()

// rest of class C here
// ...

};

Translated into pseudo-code to better highlight the working logic:

FUNCTIONS = {fun, gun, hun};
MEMBER_NAMES = {m1_, m2_, m3_};
SUFFIXES = {_1, _2, _3};

struct Data {
  auto function, member;
};

auto createFunctionName(data, suffix) {
  return data.function + suffix;
}

auto generateAccessor(data, suffix) {
  return "int " + createFunctionName(data, suffix) + "() const { return " + data.member + "." + createFunctionName(data, suffix) + "(); }";
}


class C
{

for (i = 0; i < sizeof(FUNCTIONS); ++i) {
  foreach (suffix in SUFFIXES) {
    generateAccessor(Data(FUNCTIONS[i], MEMBER_NAMES[i]), suffix);
  }
}

};
Nilsanilsen answered 18/2, 2013 at 10:30 Comment(1)
+1 nice to see that the old faithful preprocessor can help with this type of boilerplate. For aggregate classes C that also need to expose a read abstraction I would probably consider this. For my purposes, C only provides a single write abstraction, and is a behaviorless aggregate for read purposes. So I think alternative #3 serves my purposes most efficiently.Accelerometer
E
1

One other possibility is

int func(int i, int j); // i,j can be enums as well..

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.

Endear answered 18/2, 2013 at 9:11 Comment(5)
Unfortunately, the names of the member functions are not regularly named fun_ij but differ between the M_i types. What I could try is to store the M_i inside a std::tuple and at least use get<i> with an enum with named constants internally. But that doesn't really solve the multiplicity problem of Alternative 1. And then I still need to solve the 2nd layer of different member functions per type.Accelerometer
@rhalbersma if my solution with enum isnt possible, and if common interface isnt possible, then your only good choice is your alternative #1 with perhaps some macros to reduce repetition.Endear
Could you explain why you would prefer #1 to #2? Class C is an aggregate for reading, and only provides an abstraction update() for writing. Why is exposing read-only references to its building blocks so bad?Accelerometer
@rhalbersma you are right, choice between they two might be a matter of style and number of objects vs number of functions vs number of callsEndear
I have updated my question with a 3rd alternative using a std::tuple and std::get template accessor that can be called with named constants stored in an enum.Accelerometer
G
1

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) (actually fun<i,j>()) where i is an index to a member variable, and j 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.

template <typename M, int K> int fun(M const & m) const;

template <> int fun<M_1, 1>(M_1 const & m) const { return m.fun_1(); }
template <> int fun<M_1, 2>(M_1 const & m) const { return m.fun_2(); }
template <> int fun<M_1, 3>(M_1 const & m) const { return m.fun_3(); }

template <> int fun<M_2, 1>(M_2 const & m) const { return m.fun_1(); }
template <> int fun<M_2, 2>(M_2 const & m) const { return m.fun_2(); }
template <> int fun<M_2, 3>(M_2 const & m) const { return m.fun_3(); }

...

class C
{
    // Define the specialized class type for every N
    template <int N> class Mi { typedef void M; };
    template <> class Mi<1> { typedef M_1 M; };
    template <> class Mi<2> { typedef M_2 M; };
    template <> class Mi<3> { typedef M_3 M; };

    // Define the function to get the member N
    template <int N> Mi<N>::M const & get_M() const;
    template <> Mi<1>::M const & get_M() { return m1; } const;
    template <> Mi<2>::M const & get_M() { return m2; } const;
    template <> Mi<3>::M const & get_M() { return m3; } const;

    // Define the member function to call member N, function K
    template <int N, int K>
    int fun() { return fun<Mi<N>::M, K>( get_M<N>(); }

};

Now, if you want that the user can make calls with i and j as run-time variables, then this is not the way to go. Prefer an int fun(i, j) function with lots of if and switch. You cannot have both.

Gittle answered 18/2, 2013 at 9:20 Comment(2)
Thanks for writing out this direction. I had been thinking about it as well. But ultimately, this will not really reduce the amount of boilerplate that needs to be written. I think I'll stick with the std::get approach of alternative #3.Accelerometer
I won't reduce the boilerplate, but you write it, instead of the caller. As I wrote, either you write the boilerplate to avoid the caller having to know the names of the fun_XY that changes from member to member, or the caller will have to. In your question the C.get<m1>().fun_5() may fail because fun_5 is not available on m1. The caller has to care about it. In the end, this decision (you or the caller writes the boilerplate code) depends on whether there are numerous calls to the fun_XY functions, or not.Gittle
C
1

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.

class Updatable{

public:
  virtual void update() = 0;
} ; 


class M_i : public Updatable{

public:
 void update();

};

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.

  • call the delegate update directly.

.

class C{
    public:
              /** Returns the updatable interface related to M_1 */
              Updatable& getM_1Updater(){ return M_1}

              /** Returns the const reference to M_1*/
              const M_1& getM_1() const { return M_1}

              /** delegates update to each contained element */
              void update(){

                  m1.update();
                  m2.update();


                  [...]
              }

            };
Confectionary answered 18/2, 2013 at 9:58 Comment(4)
I like the Updateable interface, and in real code I would probably use that. However, I cannot expose non-const references to any of the member variables because the class C can only be updated for all its member simultaneously. It would break the class invariantst to do otherwise.Accelerometer
In this case you might consider making the update method private in all M_i classes and simply make C friend of all M_i. This allows you to safely access the non const reference to all M_i and still be sure that no one else, except C, will ever call update.Confectionary
No, the friend approach is not something that I'll consider. I simply don't want to expose the updater interface of individual building blocks to clients of C.Accelerometer
The friend relation can be limited to the container C and a variation of the Updateable interface (whose update() method should be now protected).Confectionary
N
1

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:

#include <boost/preprocessor.hpp>

// Define identifier names

#define FUNCTIONS (fun)(gun)(hun)

#define MEMBER_NAMES (m1_)(m2_)(m3_)

#define SUFFIXES (_1)(_2)(_3)


// Utility "data structure"
// Used to hand down state from iteration over functions to iteration over suffixes

#define WRAP_DATA(function, member) \
  (2, (function, member))

#define UNWRAP_DATA_FUNTION(data) \
  BOOST_PP_ARRAY_ELEM(0, data)

#define UNWRAP_DATA_MEMBER(data) \
  BOOST_PP_ARRAY_ELEM(1, data)


// Accessor-generating functionality

  // Convenience macro for generating the correct accessor name
#define CREATE_FUNCTION_NAME(data, suffix) \
  BOOST_PP_CAT(UNWRAP_DATA_FUNCTION(data), suffix)

  // Macro generating one accessor delegation
#define GENERATE_ACCESSOR(r, data, suffix) \
  int CREATE_FUNCTION_NAME(data, suffix) () const { return UNWRAP_DATA_MEMBER(data).CREATE_FUNCTION_NAME(data, suffix) (); }


// Generate accessors

class C
{

  // Execute GENERATE_ACCESSOR once for each element of SUFFIXES
#define BOOST_PP_LOCAL_MACRO(iter) \
  BOOST_PP_SEQ_FOR_EACH(GENERATE_ACCESSOR, WRAP_DATA(BOOST_PP_SEQ_ELEM(iter, FUNCTIONS), BOOST_PP_SEQ_ELEM(iter, MEMBER_NAMES)), SUFFIXES)

#define BOOST_PP_LOCAL_LIMITS (0, BOOST_PP_SEQ_SIZE(FUNCTIONS) - 1)

  // Execute BOOST_PP_LOCAL_MACRO once for each value within BOOST_PP_LOCAL_LIMITS
#include BOOST_PP_LOCAL_ITERATE()

// rest of class C here
// ...

};

Translated into pseudo-code to better highlight the working logic:

FUNCTIONS = {fun, gun, hun};
MEMBER_NAMES = {m1_, m2_, m3_};
SUFFIXES = {_1, _2, _3};

struct Data {
  auto function, member;
};

auto createFunctionName(data, suffix) {
  return data.function + suffix;
}

auto generateAccessor(data, suffix) {
  return "int " + createFunctionName(data, suffix) + "() const { return " + data.member + "." + createFunctionName(data, suffix) + "(); }";
}


class C
{

for (i = 0; i < sizeof(FUNCTIONS); ++i) {
  foreach (suffix in SUFFIXES) {
    generateAccessor(Data(FUNCTIONS[i], MEMBER_NAMES[i]), suffix);
  }
}

};
Nilsanilsen answered 18/2, 2013 at 10:30 Comment(1)
+1 nice to see that the old faithful preprocessor can help with this type of boilerplate. For aggregate classes C that also need to expose a read abstraction I would probably consider this. For my purposes, C only provides a single write abstraction, and is a behaviorless aggregate for read purposes. So I think alternative #3 serves my purposes most efficiently.Accelerometer

© 2022 - 2024 — McMap. All rights reserved.