[Trick] Container-member binding
От: remark Россия http://www.1024cores.net/
Дата: 14.12.06 20:15
Оценка: 8 (3) +1 :)

Задача

Класс (будем называть его контейнер) содержит в себе несколько членов-данных (будем называть их членами), и им (т.е. контейнеру и членам) надо общаться друг с другом — вызывать методы друг друга или писать/читать какие-то поля друг друга. Встаёт проблема: как обеспечить связь между членами и контейнером? Зачастую эта проблема встаёт при реализации т.н. свойств.
Примеры таких задач можно увидеть здесь
Автор: Денис Майдыковский
Дата: 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 — из него тоже можно большую часть вынести в базовый "фреймворковый" класс. Т.е. в целом получается не так уж и страшно.
Хотя всё ещё остаётся значительный объём ран-тайм работы при создании каждого контейнера...

FATALITY

Делаем следующую фишку. В контейнере хранятся не указатели на все члены, а смещения каждого члена относительно контейнера. А Члены хранят не указатель на контейнер, а смещение контейнера относительно члена. Сами указатели тривиально восстанавливаются по смещению. А т.к. смещения не меняются, выносим все эти данные в статические переменные и формируем их только при первом создании контейнера.
/**    Интерфейс вложенного члена */
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;
}


SCORPION WINS

Объём памяти, занимаемый членом сокращён до нуля... точнее до указателя на vtable, но он бы возможно и так был. Объём памяти занимаемый контейнером сокращён до нуля... точнее тоже до указателя на vtable, ну он бы уж точно был — что это за контейнер без виртуальных функций. Объём ран-тайм работы при создании контейнера сокращён до установки одного указателя + (выполнение одного if) * (кол-во членов) [никаких динамических выделений памяти и формирований списков]. Объём кода при описании контейнера и членов сокращён до минимума, boilerplate код, типа передачи this всем членам, практически отсутствует, всё "мясо" остаётся в базовых классах.
__declspec(thread) больше не нужен, т.к. всё статические данные формируются гарантированно во время создания глобальных объектов, т.е. в однопоточном окружении. Это происходит благодаря container_base::initializer_


з.ы. Отзывы и комментарии как всегда приветствуются


1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.