How to write read-only access functions in a summary root class?

The overall design . I have an aggregate class C containing N member variables of type M_i, i = 1 ... N , each of which has a common record update() , as well as read-only access functions to the class [F]un_i(), [F] = any letter, i = 1 .. N (in reality they do not have such regular names). Each of the element types M_i forms an independent abstraction of its own and is used elsewhere in my program.

An aggregate class must update all members in a single transaction, so it has the update() function of its own call to 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 : Do I need to access different member functions of different member variables in an aggregated class? I can come up with three alternatives :

Alternative 1 : write N * K delegate to all K member functions of all variables N

 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 access pattern to all member variables N

 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 does not allow violating the Law of Demeter , but this requires a lot of tedious boiler plate code (in my application N = 5 and K = 3 , so 15 delegation of wrappers). Alternative 2 reduces the number of wrappers, but the calling code seems a little ugly to me. But since all this code is read-only, and modfications can only happen through the serial update() aggregate, my current opinion is that alternative 2 is preferable to alternative 1 (and at least safe). If so, then all the more, alternative 3 should be the best choice, since it uses only one accessory and has the same security guarantees as alternative 2.

Question : What is the preferred interface for this type of code?

0
source share
4 answers

Turning my comment into a response.

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 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); } } }; 
+1
source

Another opportunity

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

Although you need to decide whether it matters to you. You will need to write a huge nested switch inside, 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 the general interface of M_i types.

+1
source

The solution that gives you the best user-friendly code with compilation over time requires the use of templates.

In fact, if you want to be able to call fun(i,j) (actually fun<i,j>() ), where i is the index for the member variable and j is the index for the member function of this variable, then you must define mappings. Both mappings.

  • The first mapping is between the index of the member variable and the variable itself, which implies the mapping between the index of the member variable and the type of the variable.

  • The second mapping between the index of the member function and the member function itself. However, since this mapping depends on the type of indexed member variable, it must be defined for each combination. You cannot provide the user with a fully indexed solution without defining this mapping. Or vice versa: if you do not want the caller to worry about the type of the i-th variable in order to find out what the name of the j-th function he wants to call (it depends on the type of the i-th variable), then you must provide mappings.

In this case, the user will be able to call int v = c.fun<i, j>() without knowing either the type of the i-th variable or 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 the user to be able to make calls with i and j as run-time variables, this is not the case. Prefer int fun(i, j) function with lots of if and switch . You cannot have both.

+1
source

I would completely separate update behavior from functions of one element. All M_i classes must implement the Updatable interface, which simply contains the update method.

This allows you to safely expose N accessors for (not const) updatable interfaces.

 class Updatable{ public: virtual void update() = 0; } ; class M_i : public Updatable{ public: void update(); }; 

Given the cumulative class C, you can:

  • set N accessor for classes const M_i

  • request the Updatable interface of this class M_i. By accessing this (non-constant) link, you can safely release updates for 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(); [...] } }; 
+1
source

Source: https://habr.com/ru/post/944365/


All Articles