Общий дизайн: У меня есть совокупный класс C
который содержит N
переменные-члены типа M_i, i = 1 ... N
что у каждого есть общий только для записи update()
интерфейс, а также специфичный для класса только для чтения функции доступа [F]un_i(), [F] = any letter, i = 1 .. N
(у них нет таких обычных имен в реальности). Каждый из типов членов M_i
образует собственную независимую абстракцию и используется в других местах моей программы.
Агрегатный класс должен обновить все элементы в одной транзакции, поэтому он имеет update()
функция своего собственного вызова update()
функция-член всех ее переменных-членов.
// 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_;
};
Вопрос: я могу получить доступ к различным функциям-членам различных переменных-членов в агрегатном классе? Я могу думать о три альтернативы:
Альтернатива 1: записывать N * K
делегаты всем K
функции-члены всех 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
Альтернатива 2: записывать N
доступ ко всем N
переменные-члены
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
Альтернатива 3: записывать 1
шаблон доступа для всех 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
Альтернатива 1 избегает нарушения Закон Деметры но это требует очень много утомительного кода котельной пластины (в моем приложении, N = 5
а также K = 3
, так 15
делегирование оберток). Вариант 2 сокращает количество упаковщиков, но код вызова кажется мне немного более уродливым. Но так как весь этот код доступен только для чтения, и модификации могут происходить только через согласованный агрегат update()
Мое текущее мнение, что Альтернатива 2 предпочтительнее Альтернативы 1 (и, по крайней мере, безопасна). Если это так, то тем более Альтернатива 3 должна быть лучшим выбором, поскольку она использует только один метод доступа и имеет те же гарантии безопасности, что и Альтернатива 2.
ВопросЧто является предпочтительным интерфейсом для этого типа кода?
Превращаю мой комментарий в ответ.
Если вы решили использовать альтернативу 1 (N * K делегатов), вы можете использовать Boost.Preprocessor сделать шаблонную работу за вас:
#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
// ...
};
Переведено в псевдокод, чтобы лучше выделить рабочую логику:
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);
}
}
};
Еще одна возможность
int func(int i, int j); // i,j can be enums as well..
Хотя вам нужно решить, имеет ли это смысл для вас. Вы должны написать огромный вложенный переключатель внутри, но интерфейс проще.
Этот метод, конечно, идеален, если вы можете хранить ваши объекты в массиве, и все функции-члены являются частью общего интерфейса M_i
типы.
Я бы полностью отделил поведение обновления от функциональности одного элемента. Все классы M_i должны реализовывать интерфейс Updatable, который просто содержит метод update.
Это позволяет безопасно открывать N средств доступа к (не постоянным) обновляемым интерфейсам.
class Updatable{
public:
virtual void update() = 0;
} ;class M_i : public Updatable{
public:
void update();
};
Учитывая совокупный класс C, вы можете:
выставь N аксессор к классам const M_i
попросить Обновляемый интерфейс данного класса M_i. Получив доступ к этой (неконстантной) ссылке, вы можете безопасно выпускать обновления для любого из экземпляров M_i.
.
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();[...]
}
};
Решение, которое дает вам лучший удобный код с разрешением вызовов во время компиляции, должно опираться на шаблоны.
Действительно, если вы хотите иметь возможность позвонить fun(i,j)
(на самом деле fun<i,j>()
) где i
является индексом переменной-члена, и j
индекс для функции-члена этой переменной, затем вы должны определить сопоставления. Оба отображения.
Первое сопоставление между индексом переменной-члена и самой переменной, что подразумевает сопоставление между индексом переменной-члена и типом переменной.
Второе отображение между индексом функции-члена и самой функцией-членом. Однако, поскольку это отображение зависит от типа индексированной переменной-члена, оно должно быть определено для каждой комбинации. Вы не можете предоставить пользователю полностью проиндексированное решение без определения этого отображения. Или наоборот: если вы не хотите, чтобы вызывающая сторона беспокоилась о типе i-й переменной, чтобы знать, как называется j-тая функция, которую он хочет вызвать (это зависит от типа i-й переменной), тогда вы должны предоставить сопоставления.
При этом пользователь сможет звонить int v = c.fun<i, j>()
без знания ни типа i-й переменной, ни имени j-й функции для этой i-й переменной.
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>(); }
};
Теперь, если вы хотите, чтобы пользователь мог звонить с i
а также j
как переменные времени выполнения, то это не путь. Предпочитаю int fun(i, j)
функция с большим количеством if
а также switch
, Вы не можете иметь оба.