Класс (будем называть его контейнер) содержит в себе несколько членов-данных (будем называть их членами), и им (т.е. контейнеру и членам) надо общаться друг с другом — вызывать методы друг друга или писать/читать какие-то поля друг друга. Встаёт проблема: как обеспечить связь между членами и контейнером? Зачастую эта проблема встаёт при реализации т.н. свойств.
Примеры таких задач можно увидеть
здесьАвтор: Денис Майдыковский
Дата: 20.11.01
,
здесьАвтор: Atomic Max
Дата: 17.02.05
,
здесьАвтор:
Дата: 23.08.06
и даже
здесь.
1. Выделить интерфейс контейнера и передавать this всем членам в конструктор. Члены запоминают this контейнера. Опционально сообщают контейнеру о себе. Всё, дальше они могут общаться, т.к. у каждого есть указатель на другого. Схематично:
struct IMember {};
struct IContainer
{
std::vector<IMember*> members_;
void register_member(IMember* member)
{
members_.push_back(member);
}
};
struct Member : IMemeber
{
IContainer* cont_;
Member(IContainer* cont)
: cont_(cont)
{
cont_->register_member(this);
}
};
struct Container
{
Member m1, m2, m3;
Container()
: m1(this)
, m2(this)
, m3(this)
{}
};
Что тут не нравится:
— Необходимость писать это нудное "m1(this)", особенно если членов и конструкторов много.
— Несколько смущает объём работы, совершаемый при каждом создании контейнера, — особенно формирование и хранение списка членов std::vector<IMember*>, особенно при условии, что фактически вся эта структура статическая и не меняется в рантайм, я имею в виду, что один контейнер всегда содержит в себе одинаковые члены.
Но в целом решение вполне жизненное и применяемое.
2. Решения типа такого:
#define PROPERTY(T,__type,__set,__get,__name) \
\
__type value() const { return resolve_(this,1)->__get(); } \
\
__type operator+() const { return value(); } \
\
operator __type () const { return value(); } \
\
void \
operator = (__type const& a) { resolve_(this,1)->__set(a); } \
\
template <class fake> static T* resolve_(const void* p,fake) {\
T* _ = 0; \
return (T*)( (char*)p - ( (char*)(&_->__name) - (char*)(_) ) );\
}
Не то, что бы у меня была макрософобия
Но всё же меня это немного пугает. Как-то уже становится совсем не понятно, что скрывается за всеми этими PROPERTY в описании класса, и не продебажишь...
3.
СУПЕР СвойстваАвтор: remark
Дата: 04.09.06
Многим почему-то не понравилась идея с затиранием памяти под собой...
Хотя в окончательном варианте (который я не постил) код был вполне работоспособным и даже работали вложенные контейнеры. Ну да ладно, идея с затиранием памяти всё равно остаётся слишком спорной.
Больше вразумительных идей я не видел. Самый жизненный видимо первый вариант, но избавится от нудного передавания this во все члены пока избавится не удаётся, по крайней мере я не видел решений ни здесь, ни в форумах boost, accu и т.д.
Вначале основная идея. Контейнеру и членам даётся общий базовый класс (да, Вы не ослышались), но (основная фишка) этот базовый класс уникален именно для этого контейнера и именно для его членов. У этого базового класса есть статический член, через который и может идти обмен информацией между контейнером и членами. Вот рабочий код:
/** Класс, обеспечивающий обмен между объектом-контейнером и его членами */
template<typename tag_type>
struct common_base
{
/** Для примера будем обмениваться значением int */
static int& common_data()
{
// __declspec(thread) нужен для корректной работы в многопоточном окружении
__declspec(thread) static int data = 0;
return data;
}
common_base()
{}
common_base(int data)
{ common_data() = data; }
};
/** Некий класс, который будет членом другого класса */
template<typename derived_type>
struct member : common_base<derived_type>
{
member()
{
// Можем получить, что нам передал объект-контейнер
// и как-то использовать или просто запомнить на будущее
mode = common_data();
}
/** Здесь будем запоминать, что нам передал объект-контейнер */
int mode;
};
/** Некий класс, который содержит другие */
struct container : common_base<container>
{
typedef member<container> member;
// 3 члена
member a;
member b;
member c;
container(int mode)
// Передаём значение базовому классу,
// что бы он его запомнил и передал всем членам
: common_base<container>(mode)
{}
};
int main()
{
container c1 (11); // Все члены будут содержать 11
container c2 (15); // Все члены будут содержать 15
return 0;
}
Пока контейнер просто может передать членам некоторое значение. Но уже обратите внимание достаточно удобно — контейнер передаёт нужное значение в конструктор базовго класса "common_base<container>(mode)", а члены читают его в конструкторе "mode = common_data()".
Теперь сделаем возможность параметризовать common_base типом разделяемых данных, что бы решение было более повторно используемым, и плюс к этому обеспечим возможность обмениваться данными между членами и контейнером на всё время жизни:
/** Класс, обеспечивающий обмен между объектом-контейнером и его членами */
template<typename tag_type, typename data_type>
struct common_base
{
/** Для примера будем обмениваться значением int */
static data_type& common_data()
{
__declspec(thread) static data_type data = 0;
return data;
}
common_base()
{}
common_base(data_type data)
{ common_data() = data; }
};
/** Некий класс, который будет членом другого класса */
template<typename derived_type, typename data_type>
struct member : common_base<derived_type, data_type>
{
member()
{
// Можем получить, что нам передал объект-контейнер
// и как-то использовать или просто запомнить на будущее
data = common_data();
}
/** Здесь будем запоминать, что нам передал объект-контейнер */
data_type data;
};
/** Некий класс, который содержит другие */
struct container : common_base<container, int*>
{
typedef member<container, int*> member;
// 3 члена
member a;
member b;
member c;
// Переменная разделяется между этим объектом
// и его членами (члены имеют указатель на неё)
// Т.о. все могут через неё обмениваться данными
int shared;
container()
// Передаём значение базовому классу,
// что бы он его запомнил и передал всем членам
: common_base<container, int*>(&shared)
, shared(0)
{}
};
int main()
{
container c1;
container c2;
return 0;
}
Теперь поле int container::shared становится как бы разделяемым между контейнером и членами на всё время их жизни — у всех есть указатель на него.
Теперь реализуем непосредственно возможность общаться контейнеру и членам, т.е. у каждого члена будет указатель на контейнер, а у контейнера указатели на все его члены. Сделаем интерфейсы контейнера и члена. common_base теперь будет передавать всем членам указатель на контейнер:
/** Интерфейс вложенного члена */
struct i_member
{
virtual void member_do_some() {};
};
/** Интерфейс контейнера */
struct i_container
{
virtual void container_do_some() {};
};
/** Класс, обеспечивающий обмен между объектом-контейнером и его членами */
template<typename tag_type>
struct common_base
{
/** Возвращает указатель на текущий конструируемый объект-контейнер */
static tag_type*& common_data()
{
__declspec(thread) static tag_type* data = 0;
return data;
}
/** Конструктор, который вызывают члены */
common_base(i_member* member)
{
// Добавляем член в общий список
common_data()->members.push_back(member);
}
/** Конструктор, который вызывает контейнер */
common_base(tag_type* self)
{
// Устанавливаем указатель на текущий конструируемый объект
common_data() = self;
}
};
/** Базовый класс для контейнеров членов, но не для самих членов */
template<typename tag_type>
struct container_base : i_container, common_base<tag_type>
{
container_base()
: common_base<tag_type>(static_cast<tag_type*>(this))
{}
// Список всех членов
std::list<i_member*> members;
};
/** Некий класс, который будет членом другого класса */
template<typename derived_type>
struct member : i_member, common_base<derived_type>
{
member()
: common_base<derived_type>(this)
, container(common_data())
{}
// Здесь запоминаем указатель на свой контейнер
derived_type* container;
void g()
{
// При необходимости можем обратиться к своему контейнеру
container->f();
}
};
/** Некий класс, который содержит другие */
struct container : container_base<container>
{
typedef member<container> member;
// 3 члена
member a;
member b;
member c;
void f()
{
// При необходимости можем обратиться ко всем своим членам
std::for_each(members.begin(), members.end(), boost::bind(&i_member::member_do_some, _1));
}
};
int main()
{
container c1;
container c2;
c1.f();
c1.a.g();
return 0;
}
Yep! Члены получают доступ к указателю на контейнер без явной его передачи! Правда код достаточно усложнился, хотя надо учитывать, что большая часть этого кода должна находится в неком фреймворке, а не в пользовательском коде — в пользовательском коде находится только класс container — он максимально тривиален, а так же класс member — из него тоже можно большую часть вынести в базовый "фреймворковый" класс. Т.е. в целом получается не так уж и страшно.
Хотя всё ещё остаётся значительный объём ран-тайм работы при создании каждого контейнера...
Делаем следующую фишку. В контейнере хранятся не указатели на все члены, а
смещения каждого члена относительно контейнера. А Члены хранят не указатель на контейнер, а
смещение контейнера относительно члена. Сами указатели тривиально восстанавливаются по смещению. А т.к. смещения не меняются, выносим все эти данные в статические переменные и формируем их только при первом создании контейнера.
/** Интерфейс вложенного члена */
struct i_member
{
virtual void member_do_some() {};
};
/** Интерфейс контейнера */
struct i_container
{
virtual void container_do_some() {};
};
/** Класс, обеспечивающий обмен между объектом-контейнером и его членами */
template<typename tag_type>
struct common_base
{
/** Возвращает указатель на текущий конструируемый объект-контейнер */
static tag_type*& common_data()
{
static tag_type* data = 0;
return data;
}
// Список всех членов
// Только теперь храним не указатели на конкретные переменные,
// а смещение каждого члена относительно контейнера,
// тогда мы в рантайм всегда сможем восстановить как адрес члена по
// адресу контейнера, так и наоборот
static std::list<int>& members()
{
static std::list<int> data;
return data;
}
/** Применить функтор ко всем членам */
template<typename functor>
void for_each_member(functor func)
{
std::list<int>::const_iterator i = members().begin();
for (; i != members().end(); ++i)
{
i_member* member = reinterpret_cast<i_member*>(
reinterpret_cast<char*>(static_cast<tag_type*>(this)) + *i);
func(*member);
}
}
/** Конструктор, который вызывают члены */
common_base()
{}
/** Конструктор, который вызывает контейнер */
common_base(tag_type* self)
{
// Устанавливаем указатель на текущий конструируемый объект
common_data() = self;
}
};
/** Базовый класс для контейнеров членов, но не для самих членов */
template<typename tag_type>
struct container_base : i_container, common_base<tag_type>
{
container_base()
: common_base<tag_type>(static_cast<tag_type*>(this))
{
initializer_.fake();
}
/** Класс обеспечивает создание одного экземпляра каждого контейнера
* при инициализации глобальных переменных в однопоточном окружении
*/
struct initializer
{
initializer()
{
// Здесь собственно создаём контейнер
tag_type obj;
(void)obj;
}
void fake() {}
};
static initializer initializer_;
};
template<typename tag_type> typename container_base<tag_type>::initializer container_base<tag_type>::initializer_;
/** Базовый класс уже для членов */
template<typename derived_type, typename tag_type>
struct member_base : i_member, common_base<derived_type>
{
member_base()
{
// Проверяем, что конструктор вызвался первый раз
// Во все остальные разы условие не будет срабатывать
if (-1 == offset())
{
// Запоминаем своё смещение относительно контейнера
offset() = reinterpret_cast<char*>(this)
- reinterpret_cast<char*>(common_data());
// Передаём своё смещение в контейнер
members().push_back(offset());
}
}
/** Получить своё смещение относительно контейнера */
static int& offset()
{
static int offset = -1;
return offset;
}
/** Получить указатель на контейнер */
derived_type* get_container()
{
return reinterpret_cast<derived_type*>(
reinterpret_cast<char*>(this) - offset());
}
};
/** Некий класс, который будет членом другого класса */
template<typename derived_type, typename tag_type>
struct member : member_base<derived_type, tag_type>
{
void g()
{
// При необходимости можем обратиться к своему контейнеру
get_container()->f();
}
};
/** Некий класс, который содержит другие */
struct container : container_base<container>
{
// 3 члена
struct a_tag;
member<container, a_tag> a;
struct b_tag;
member<container, b_tag> b;
struct c_tag;
member<container, c_tag> c;
void f()
{
// При необходимости можем обратиться ко всем своим членам
for_each_member(boost::bind(&i_member::member_do_some, _1));
}
};
int main()
{
container c1;
container c2;
c1.f();
c1.a.g();
return 0;
}
Объём памяти, занимаемый членом сокращён до нуля... точнее до указателя на vtable, но он бы возможно и так был. Объём памяти занимаемый контейнером сокращён до нуля... точнее тоже до указателя на vtable, ну он бы уж точно был — что это за контейнер без виртуальных функций. Объём ран-тайм работы при создании контейнера сокращён до установки одного указателя + (выполнение одного if) * (кол-во членов) [никаких динамических выделений памяти и формирований списков]. Объём кода при описании контейнера и членов сокращён до минимума, boilerplate код, типа передачи this всем членам, практически отсутствует, всё "мясо" остаётся в базовых классах.
__declspec(thread) больше не нужен, т.к. всё статические данные формируются гарантированно во время создания глобальных объектов, т.е. в однопоточном окружении. Это происходит благодаря container_base::initializer_
з.ы. Отзывы и комментарии как всегда приветствуются