Pull to refresh

Comments 42

Linked list состоящий из 2х элементов?
Linked list обычно связывает объекты одного и того же типа.
Не силён в сях, но по-моему вы изобрели медиатор.
Для медиатора нужен третий объект.

А так — да, чем-то похоже. Только медиатор может ещё инкапсулировать в себе поведение — управлять связываемыми объектами, а у этой штуки задача простая — «кто, бишь, мой сосед?»
Приведите пожалуйста пример практического применения.
Например, в какой-нибудь игре могли бы быть такие связи: «игровой персонаж» (Player) — «транспортное средство» (Vehicle) и «транспортное средство» (Vehicle) — «пушка» (Weapon).

Что можно было бы реализовать с использованием представленного шаблона так:
class Player: public O<Player, Vehicle> {};
class Vehicle: public O<Vehicle, Player>, public O<Vehicle, Weapon> {};
class Weapon: public O<Weapon, Vehicle> {};


А дальше:
1. Сел игрок-1 в машину-1:
p1->SetLink(v1);
2. Поставил на машину-1 пушку-1:
p1->GetLink<Vehicle>()->SetLink(w1);
3. Потом пересел из машины-1 в машину-2:
p1->SetLink(v2);
Я бы так не делал.
Первое — я бы использовал умные указатели.
Второе — делал бы методы с говорящими именами. player->SitInCar( car ); К тому же в этом случае легко реализовать дополнительную логику.
Кстати да, и с учетом умных указателей появляется дополнительная и очень полезная логика «кто-держит-кого», в одну сторону shared_ptr, в другую weak_ptr и так далее, любые вариации. Удалили человека — пропала и собака :)
Это уже получаются не равноправные отношения, а отношения «владения». У меня слегка иная семантика.
> 1. Сел игрок-1 в машину-1:
> p1->SetLink(v1);
> 2. Поставил на машину-1 пушку-1:
> p1->GetLink()->SetLink(w1);

И автоматом вылез из машины. Извините, место одно — или игрок в машине, или пушка :)
Я не хочу сказать, что вашей связи нет применения, но в случае с игрой действительно обычно не 1 связь у объекта. Нужно поискать более практичный пример. Мне кажется, что это слишком узкоспециализированный шаблон (и при этом достаточно простой) чтоб его описывать в книгах.
Прошу прощения, про v.4 (множественные ссылки) не прочитал.
Тем не менее, множественное наследование не слишком практично в использовании. Ну тот же пример с игрой, но в машину может сесть 4 человека, а не 1.
но в машину может сесть 4 человека, а не 1

Вопросов нет, данный шаблон рассчитан только на связи 1 к 1.
Глазом моргнуть не успеешь, как он должен будет быть на это рассчитан ) Причём «вчера»

Я высказал своё мнение по-подробнее в этом комментарии
Как же в этой связи мне нравится Qt.

Скажем, если человеку нужно завести несколько собак, то приведённый код надо переписывать с нуля. А если у каждой собаки может быть несколько владельцев одновременно, то слежение за всеми ссылками превращается в сущий кошмар.

А в Qt если необходимо установить связь между объектами, совсем не обязательно в объекте хранить указатели на соседа. Например:
connect(humanObject, SIGNAL(giveCandies(int)), dogObject, SLOT(eatCandies(int)));
connect(dogObject, SIGNAL(bark()), humanObject, SLOT(runAway()));

Соответственно, если humanObject или dogObject удалить, то связь будет разрушена и методы вызываться не будут.
И как получить здесь получить всех собак человека?
Если кратко, то никак. А зачем в определённый момент может понадобиться список всех собак? Если для того, чтобы выполнить какой-нибудь метод или вызвать деструктор, то сигналы-слоты с этим нормально справляются. Всю логику отношений N к N можно выразить таким образом.
Например, человеку нужно заплатить налог с каждой собаки пропорционально длине хвоста. Если послать сигнал «измерить длину хвоста», то в общем случае неизвестно, когда закончится последний слот.

Кажется, кто-то сказал шёпотом «Visitor», или мне послышалось?
Хм, конечно же хранит, но это происходит вне объекта (где-то в QApplication, под рукой у основного цикла событий). Речь шла о том, что контроль за целостностью указателя программистом не осуществляется в принципе.
Хранит конечно, но получить через существующее апи в Qt невозможно.
Я общался по этому поводу с их разработчиками в своё время.
Сделано так из соображений дизайна взаимодействий.
Вы изобрели котопса )).
P.S.: Просто первая ассоциация после прочтения.
Реализовывал нечно подобное, но на макросах и с указанием названия методов для более читабельного API. Очень удобный подход.
>>И тут мне подумалось: «надо это переделать на шаблоны!»
Нужно как-то себя ловить на этой мысли и пытаться сдерживать до появления действительно веских причин для использования шаблонов :)
А так, забавно, но не более того…
Для перегрузки методов родительских классов «внутри» потомка при множественном наследовании достаточно ввести имена методов родительских классов в class scope класса-потомка.

class Human: public O<Human, Dog>, O<Human, Cat>
{
public:
    using O<Human, Dog>::Setlink;
    using O<Human, Cat>::Setlink;
};
Да. Изначально у меня так и было.

Но поскольку с GetLink() такой фокус не прокатывает, я решил сделать шаблоном.

Да и потом… если связей будет больше — это для каждой связи нужно писать:
using O<Human, ...>::SetLink;

А так можно обойтись всего одной шаблонной реализацией.
один человек может владеть максимум одной собакой/кошкой/сепулькой..?
«Класс В есть наследник класса А» означает, что всякий В является одновременно и А.
Всякий человек является поводком?)
Согласен, принцип «всякое наследование — это отношение вида „является“» здесь некоторым образом игнорируется.

Но можно и перефразировать: «всякий человек является потенциальным хозяином/соседом собаки/сепульки».

В принципе, у меня был вариант реализации данной схемы не через наследование, а через композицию, когда экземпляр O<Human, Dog> явлется членом класса Human. Но в этом случае усложняется код инициализации — в конструкторе класса Human приходится инициализировать объект O<Human, Dog> указателем this, и усложняется «пользовательский» код — вместо h1->SetLink(d1); надо писать h1->GetLinker()->SetLink(d1);.
Я задумался над записью:
class Human: public O<Human, Dog>, O<Human, Cat> {};

Подозреваю что правильная запись:
class Human: public O<Human, Dog>, public O<Human, Cat> {};

Иначе второе наследование будет private. Как помню отыскать такую ошибку позже при уже рабочем использовании классов+шаблонов+пару_наследований будет очень не просто по специфике сообщений компилятора о шаблонах.
Точно!

Вот что значит писать статью одновременно с доработкой кода. Обязательно забудешь что-нибудь привести в соответствие с рабочим вариантом.

Спасибо! Поправил.
На паттерн проектирования не тянет всё-таки IMHO, но в качестве практических занятий с шаблонами в С++ покатит :)
Идея зацепила, пока не знаю к чему бы применить, но, вспоминая труды Александреску :) пришло в голову некоторое обобщенное решение с применением списков типов.
В Списке типа можно указать все типы, с которыми мы хотим связываться, в том числе и с собой.
Данное решение писалось под MSVC++ 2005, доберусь до gcc и там проверю. Зачастую бывает писанное в студии с первого раза не компилится в gcc.
Данное решение не проверяет если пользователь в один список типов включил несколько одинаковых типов.
В общем если кому интерсно, то вот еще + 1 вариант к предложенному автором.

#include <iostream>

#include <vector>
#include <algorithm>

template <typename H, typename T>
struct TypeList
{
	typedef H Head;
	typedef T Tail;
};

struct NullType
{
};

#define TYPE_LIST_1(t1) \
	TypeList<t1, NullType>

#define TYPE_LIST_2(t1, t2) \
	TypeList<t1, TYPE_LIST_1(t2) >

#define TYPE_LIST_3(t1, t2, t3) \
	TypeList<t1, TYPE_LIST_2(t2, t3) >

#define TYPE_LIST_4(t1, t2, t3, t4) \
	TypeList<t1, TYPE_LIST_3(t2, t3, t4) >

#define TYPE_LIST_5(t1, t2, t3, t4, t5) \
	TypeList<t1, TYPE_LIST_4(t2, t3, t4, t5) >

template <typename T>
struct TypeWrap
{
};

template <typename TClass, typename TLinkedClasses>
class LinkHolder
	: public LinkHolder<TClass, typename TLinkedClasses::Tail>
{
public:
	typedef typename TLinkedClasses::Head LinkType;
	virtual ~LinkHolder()
	{
		LinkPool Tmp = Links;
		for (LinkPool::const_iterator i = Tmp.begin() ; i != Tmp.end() ; ++i)
			(*i)->DelLink(static_cast<TClass *>(this));
	}
	using LinkHolder<TClass, typename TLinkedClasses::Tail>::AddLink;
	using LinkHolder<TClass, typename TLinkedClasses::Tail>::DelLink;
	void AddLink(LinkType *link)
	{
		if (std::find(Links.begin(), Links.end(), link) != Links.end())
			return;
		Links.push_back(link);
		link->AddLink(static_cast<TClass *>(this));
	}
	void DelLink(LinkType *link)
	{
		LinkPool::iterator Iter = std::find(Links.begin(), Links.end(), link);
		if (Iter == Links.end())
			return;
		LinkType *Link = *Iter;
		Links.erase(Iter);
		Link->DelLink(static_cast<TClass *>(this));
	}
	template <typename T>
	unsigned GetLinkCount() const
	{
		return GetLinkCountImpl(TypeWrap<T>());
	}
	template <typename T>
	T* GetLink(unsigned index) const
	{
		return GetLinkImpl(TypeWrap<T>(), index);
	}
protected:
	using LinkHolder<TClass, typename TLinkedClasses::Tail>::GetLinkCountImpl;
	unsigned GetLinkCountImpl(const TypeWrap<LinkType> &) const
	{
		return static_cast<unsigned>(Links.size());
	}
	using LinkHolder<TClass, typename TLinkedClasses::Tail>::GetLinkImpl;
	LinkType* GetLinkImpl(const TypeWrap<LinkType> &, unsigned index) const
	{
		return Links.size() > index ? Links[index] : 0;
	}
private:
	typedef std::vector<LinkType *> LinkPool;
	LinkPool Links;
};

template <typename TClass>
class LinkHolder<TClass, NullType>
{
public:
	virtual ~LinkHolder()
	{
	}
	void AddLink()
	{
	}
	void DelLink()
	{
	}
	unsigned GetLinkCountImpl(const TypeWrap<NullType> &) const
	{
		return -1;
	}
	NullType* GetLinkImpl(const TypeWrap<NullType> &, unsigned) const
	{
		return 0;
	}
};

class A;
class B;
class C;

class A
	: public LinkHolder<A, TYPE_LIST_3(A, B, C)>
{
public:
	void A2B();
	void Stop();
};

class B
	: public LinkHolder<B, TYPE_LIST_3(A, B, C)>
{
public:
	void B2C();
};

class C
	: public LinkHolder<C, TYPE_LIST_3(A, B, C)>
{
public:
	void C2A();
};

void A::A2B()
{
	std::cout << __FUNCTION__ << std::endl;
	unsigned Count = GetLinkCount<B>();
	for (unsigned i = 0 ; i < Count ; ++i)
		GetLink<B>(i)->B2C();
}

void A::Stop()
{
	std::cout << __FUNCTION__ << std::endl;
}

void B::B2C()
{
	std::cout << __FUNCTION__ << std::endl;
	unsigned Count = GetLinkCount<C>();
	for (unsigned i = 0 ; i < Count ; ++i)
		GetLink<C>(i)->C2A();
}

void C::C2A()
{
	std::cout << __FUNCTION__ << std::endl;
	unsigned Count = GetLinkCount<A>();
	for (unsigned i = 0 ; i < Count ; ++i)
		GetLink<A>(i)->Stop();
}

int main()
{
	A a;
	std::auto_ptr<B> b(new B);
	C c;

	a.AddLink(&a);
	a.AddLink(b.get());
	a.AddLink(&c);

	b->AddLink(&a);
	b->AddLink(b.get());
	b->AddLink(&c);

	c.AddLink(&a);
	c.AddLink(b.get());
	c.AddLink(&c);
	
	a.A2B();

	b.reset(0);

	std::cout << std::endl;
	a.A2B();
}

Отличное обобщение!

Чтобы это собрать в gcc нужно применить такой diff:
--- a/typelists.cpp
+++ b/typelists.cpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 #include <algorithm>
+#include <memory>
 
 template <typename H, typename T>
 struct TypeList
@@ -43,7 +44,7 @@ public:
        virtual ~LinkHolder()
        {
                LinkPool Tmp = Links;
-               for (LinkPool::const_iterator i = Tmp.begin() ; i != Tmp.end() ; ++i)
+               for (typename LinkPool::const_iterator i = Tmp.begin() ; i != Tmp.end() ; ++i)
                        (*i)->DelLink(static_cast<TClass *>(this));
        }
        using LinkHolder<TClass, typename TLinkedClasses::Tail>::AddLink;
                        (*i)->DelLink(static_cast<TClass *>(this));
        }
        using LinkHolder<TClass, typename TLinkedClasses::Tail>::AddLink;
@@ -57,7 +58,7 @@ public:
        }
        void DelLink(LinkType *link)
        {
-               LinkPool::iterator Iter = std::find(Links.begin(), Links.end(), link);
+               typename LinkPool::iterator Iter = std::find(Links.begin(), Links.end(), link);
                if (Iter == Links.end())
                        return;
                LinkType *Link = *Iter;
typename то я зачастую и забываю… gcc быстро напоминает об этом :)
Лучше бы сделать у этого шаблона виртуальный деструктор, раз вы от него наследуетесь.
Справедливое замечание.

Но это будет необходимо только в случае, если где-то в коде я буду держать полиморфный указатель O<Human,Dog>* (или другой базовый класс), а потом захочу его уничтожить. Но пока я не могу придумать случаев, когда вместо просто Human* я хотел бы использовать именно O<Human, что-то>*.
Мне не нравится данное решение, потому что мне не нравится сам подход хранения ссылок друг на друга. Это типичное отношение многие-ко-многим (человек может иметь много домашних животных и домашнее животное может иметь много хозяев). В реляционных БД в таких случаях заводится третья таблица и отношение многие-ко-многим сводится к двум отношениям один-ко-многим. В реальности отношение человека с домашним животным является внешней характеристикой по отношению к ним обоим: нельзя сказать, что наличие/отсутствие домашнего животного это неотъемлемая характеристика человека. Наличие животного это не цвет глаз. Может ещё ссылку на перочинный ножик в Human положить? Класс Person/Human/Whatever не должен иметь поля, указывающего на животных или указывающего на отсутствие животных.

Я думаю, что ваш подход ещё вызовет у вас головную боль и предлагаю одуматься:

class Person
{
};

class Pet
{
};

class PetOwnerRelations
{
public:
        vector<Pet*> petsOfOwner(Person* owner);
        vector<Person*> ownersOfPet(Pet* pet);
};
Касательно многие-ко-многим мы тут уже обсудили выше. Да, эта реализация поддерживает только 1:1 by design. Связи типа 1:1 в нашем с вами мире так же реальны как и M:M. Возможно, пример с питомцами не очень удачен.

С другой стороны — вариант для M:M (M != const) предложен выше. Лично мне ещё интересен вариант M:M, где M == const.

Внешнее хранение ссылок мне в голову тоже приходило. Но даже в этом случае придется модифицировать классы Person и Pet — как минимум придется добавить в деструктор каждого что-то вроде PetOwnerRelations::Remove(this);. Так что совсем внешней реализации связи, обладающей описанными в статье свойствами, не получается.

С другой стороны, если PetOwnerRelations сделать шаблонным — может выйти вполне универсальное решение. Другое дело, что тогда для связи между каждой парой типов придется заводить отдельный экземпляр соответствующего класса XRelations. Возможно, в некоторых случаях это будет лучшее решение, но я не уверен, что во всех.
Only those users with full accounts are able to leave comments. Log in, please.