How to write read-only accessor functions in an ag

2019-08-31 12:12发布

问题:

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?

回答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);
  }
}

};


回答2:

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.



回答3:

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();


                  [...]
              }

            };


回答4:

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.