Pull to refresh

Comments 82

Разве нельзя в данной ситуации обойтись множественным наследованием?
class Flying { /* ... */ };
class Quacking { /* ... */ };
class FlyingAndQuacking : public Flying, public Quacking { /* ... */ };

class ReadheadDuck : public FlyingAndQuacking { /* ... */ };
class RubberDuck : public Quacking { /* ... */ };
А как потом в методе указать несколько интерфейсов у объекта? Если объект отнаследовать отинтерфейсов A, B и С, то как указать в типе параметра, что нужны только интерфейсы A и С?
Вообще, мне кажется, что такая потребность возникает очень и очень редко. Если вам сильно нужно указывать в разных методах все возможные сочетания интерфейсов, вы можете создать себе следующие комбинации интерфейсов:
AB: A, B
BC: B, C
AC: A, C
ABC: AB, BC, AC (виртуальное наследование)

Наследуя объект от ABC, вы теперь можете задать в сигнатурах методов любые комбинации интерфейсов. Не проверял :), но мне кажется, что это должно работать.

Впрочем, я не уверен, что этот метод лучше вашего. Просто интересно, рассматривали ли вы его и в чем плюсы/минусы.
Проблема в том, что это как раз не работает. См. мой коммент ниже…
Но мой способ отличается от приведенного вами ниже. Вчитайтесь.
Фактически шаблон IGroup делает это за вас. Он порождает дерево наследования от всевозможных комбинаций интерфейсов. Для интерфейсов A,B,C он породит родителей ABC, AB, AC, BA, BC, CA, CB, A, B, C.
Вот, уже видна кое-какая разница. В вашем решении генерируются все размещения интерфейсов (кстати, поправьте в статье), а в приведенном мной — все сочетания.

Опять же, я прекрасно понимаю ваше решение. Я просто пытаюсь узнать, рассматривали ли вы плюсы и минусы обоих подходов.
В начале статьи у меня код для сочетаний, а в конце для размещений. Просто одних сочетаний не достаточно, порядок наследования имеет значение. Не смотря на то, что речь идёт об интерфейсах, а не о полноценных классах (с полями).

Спасибо, поправлю термины выборки и сочетания, на сочетания и размещения. Запутался я немного)
Да-да, одних только сочетаний в вашем подходе недостаточно, нужно перечислять все размещения, чтобы сделать решение общим и универсальным.

А я намекал на то, что в моем случае это как раз необязательно, достаточно перечислить все сочетания, так как, если существует интерфейс AB, использовать BA в коде не получится: он просто-напросто нигде не определен.

Более того, даже необязательно определять все сочетания сразу, можно добавлять их по мере необходимости.
Понятно. Мы говорим об одном и том же. Только у Вас подход заключается в том, чтобы проименовать группы интерфейсов явным образом. Я же, получается, предложил способ автоматизации этого процесса.
class A {};
class B {};
class C {};

class Somebody : public A, public B, piblic C {};

class AC : public A, public C {}

void methodAC(AC * one)
{
}

int main()
{
   Somebody * smb = new Somebody();
   methodAC(smb); <u>// Ошибка компиляции ...</u>
}
В целом, спасибо за статью :) Подход действительно интересный.
Вы пытаетесь перенести методы программирования динамических языков в С++. Ничего хорошего из этого не получится.
Не совсем, в языке С++ могла бы быть конструкция типа такой:
void methodAB( {A*,B*} one );
или даже
void methodAB( {A,B} * one );
Вот здесь кроется ваша ошибка проективания. Смысл в том, что вам неважно внутри функции, какой именно класс будет подан на вход А или В, следовательно вы будете использовать нечто общее из этих классов, следовательно вынесите просто эту общую часть в отдельный базовый класс и не страдайте хернёй.

Далее, если вы будете следовать простому базовому правилу проектирования «одна сущность — один класс», то у вас не возникнет таких проблем вообще. Правильная иерархия вкупе с Александреску и базовыми шаблонами проектирования (Gamma et al.) дадут вам всё, что нужно без таких вот извращений.
Вы путаете. Не «вам неважно внутри функции, какой именно класс будет подан на вход А или В, следовательно вы будете использовать нечто общее из этих классов, следовательно вынесите просто эту общую часть в отдельный базовый класс и не страдайте хернёй», а наоборот — будет использоваться и функционал A, и функционал B.

Так а зачем это выносить в один метод? Один конкретный метод решает одну маленьку конкретную задачу. Разбейте этот метод на 2: один использует функционал А, другой — В. Если не получается — у Вас явная ошибка в проектировании.
«Так а зачем это выносить в один метод?»
Потому что оно всегда используется вместе в заданной последовательности, например.

«Разбейте этот метод на 2: один использует функционал А, другой — В.»
… а потом напишите группирующий метод, который вызовет их в заданной последовательности. И в нем все равно придется делать такое ограничение.
> … а потом напишите группирующий метод
Это признак ошибки в проектировании, как я уже и говорил.
«Это признак ошибки в проектировании, как я уже и говорил.»
Не понимаю, почему. Ну вот есть у меня функционал, который применим только к объектам, обладающим признаками A и B. Почему (если мне позволяет язык) я не могу сделать это ограничением входного параметра?
Использование шаблона IGroup позволяет при увеличении глубины вызова постепенно отбрасывать лишние интерфейсы. Иными словами, сперва у нас тип производный от A,B и C. Затем останется A,B и уже в самом конце вызовутся методы, которые принимают только A или только B.
А где это применимо? Даже если и возникает такая ситуация, неужели настолько трудно создать интерфейс IAC и использовать такой же подход, как и в COM?

Где-то ниже уже говорили о использовании буста, для того, чтобы наложить ограничения на передаваемый в функцию объект. Если вы не хотите его за собой тянуть — dynamic_cast и assert вам в помощь.
Ну так а на каком этапе сработает assert? На этапе выполнения… А тут ошибки детектируются на этапе компиляции.
Опять-таки, где это применимо? Писать велосипеды — это весело и интересно, но вот, скажите, зачем? Одно из первых правил при разработке ПО: если что-то можно сделать простым и общепринятым способом — сделай так, не надо городить.
Я думал об этом, и совершенно согласен с dslf, это ошибка проектирования. Один задача — одна функция. Например, выше автор приводит пример method (AB *), в этом случае вы косвенно хотите знать тип передаваемого указателя, что прямо указывает, что вы ошиблись в проектировании. Кроме того, вся работа должна выполняться в методах класса, внешние функции должны всего лишь дёргать нужные методы класса, поэтому пример выше отлично разбивается на две функции и, скорей всего, он не нужен вообще.

У автора так же есть одна большая проблема с количеством типов. Данная идиома IGroup совершенно не расширяема (точнее, это грустное занятие выписывать все сочетания), кроме того загрязняется объект. Если возникнет ошибка компилятор завалит вас дичайшими сообщениями об ошибках.

2k06a: Кроме того, действительно сложно себе представить ситуацию, когда такое необходимо (абстрактные утки слишком абстрактны). Давеча я столкнулся с подобной проблемой, но отлично обошёлся Visitor-ом и сохранил гибкость. Мне больше всего хотелось избавиться от dynamic_cast, так как это сущий тормоз и несколько вызовов по виртуальной таблице существенно быстрее.
Насчёт расширяемости: Вполне себе расширяемо и переносимо. Можно сделать скрипт, который генерирует IGroup вплоть до 10 параметров, сохранить это чудо в IGroup.h и использовать без каких-либо изменений в любых проектах.

Насчёт скорости «вызовов по виртуальной таблице»: У меня такой вопрос давно уже зародился. Насколько вызов виртуального метода в C++ медленнее вызова простого метода? Насколько я понимаю добавляется лишь одна инструкция JMP и это не зависит от глубины наследования. Верно?
«Один задача — одна функция. „
Да. Которая работает с объектом, обладающим двумя признаками. В чем проблема?

“Например, выше автор приводит пример method (AB *), в этом случае вы косвенно хотите знать тип передаваемого указателя, что прямо указывает, что вы ошиблись в проектировании»
Не понимаю, почему. Я не хочу знать тип объекта, я хочу знать признаки, которыми он обладает (точнее, гарантировать, что он обладает набором признаков, еще на этапе компиляции), и использовать связанную с этими признаками функциональность.

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

Проблемы автора — проблемы реализации. А я говорю о подходе.
Возможно стоило поместить в блог «Ненормальное программирование?»)
Динамические языки тут ни при чем. Тип объекта известен на этапе компиляции, известно от каких интерфейсов он наследуется, все проверяется компилятором.
> но таковой конструкции ни в одном ЯП не встречал

Generics в Java

<T extends Quackable & Duck> void doFlyQuacking(T one)
{
  // ...
}

А ведь помимо extends есть ещё и маска super…

В некоторых вопросах джавовские генерики поражают возможностями.
ключевое слово where для дженерик-метода:

interface IA { void A(); }
interface IB { void B(); }

class C
{
public void Foo(T obj)
where T : IA, IB
{
obj.A();
obj.B();
}
}
Не знал что where можно использовать в объявлении методов. Спасибо за информацию.
Хаа, и я попался :)
...void Foo<T>(T obj)…
тоже парсер уголки скушал, да? ;)
Да :) Я каждый раз на эту фичу попадаюсь.
Дженерики это недоделанные шаблоны:
template< class T >
void doFlyQuacking( T *)
{
    static_assert( std::is_base_of< IQuackable, T >::value );
    static_assert( std::is_base_of< IFlyable, T >::value );
}

Только одна проблема, вероятно в данном случае использовать шаблоны не очень хочется:
1. медленно компилируется, если это в .h файле
2. для экспорта функции (или для переноса в cpp) придется явно указать, с какими типами (T) ее будут использовать
Спасибо за этот пример, добавил Ваш коммент в избранное.
Только порядок параметров обратный вроде должен быть: <T,IQuackable>
Пардон, ассерту же нужен 0, который false.
Тогда стоит написать так:
static_assert( !std::is_base_of<T, IQuackable>::value );
Это ведь не то же самое, что и:
static_assert( std::is_base_of<IQuackable, T>::value );
std::is_base_of<T, IQuackable>::value будет всегда false,
т.к. IQuckable нифига не наследуется от T:

msdn.microsoft.com/en-us/library/bb982243.aspx

Единственное, надо добавить в static_assert сообщение об ошибке:
static_assert( std::is_base_of< IQuackable, T >::value, «T must be a kind of IQuackable» );
Пардон, что-то я запутался)
Хотелось бы увидеть задачу, где это действительно понадобилось. И к примеру почему бы не использовать к примеру списки типов?
> но таковой конструкции ни в одном ЯП не встречал
генерики в Java или C#, например.
в плюсах, как по мне лучше было бы заюзать Boost Concept Check:
#include <boost/concept_check.hpp>
#include <boost/concept/requires.hpp>

template <class T>
BOOST_CONCEPT_REQUIRES(
  ((boost::Convertible<T, Quackable>)) 
  ((boost::Convertible<T, Flyable>)),
  (void)
) doFlyQuacking(T *one) {
...
}

кстати, в принципе можно обойтись и просто с помощью boost type traits, но будет более многословно
Да ну, и чем генерики отличаются от темплейтов в данном случае? Вот duck-typing и mix-ins да
Простите, не понял к чему тут duck-typing и mix-in'ы. Ни я ни автор про это не говорили
Не так понял, не обращай внимания
Спасибо за пример, прокачаю свои знания буста.
>Вообще этот приём мог быть языковой конструкцией, но таковой конструкции ни в одном ЯП не встречал.
Haskell typeclasses, вроде бы, как раз эта самая конструкция.
Интересно, на что автор статьи вообще смотрел — в комментах привели уже 4 разных реализации идеи в разных языках.
«Вообще этот приём мог быть языковой конструкцией, но таковой конструкции ни в одном ЯП не встречал.»
Constrained generics в CLR.
хех :) уже 3й человек вспомнил про них (и это не считая Java)
Виртуальное наследование… хм… разработчики компиляторов на него ругаются! Да и вообще, мне кажется, что компилятор не будет эффективный код генерить из таких языковых конструкций.
Физически получится наследование от 3-х базовых интерфейсов. Не более того. Для того и есть слово virtual.
Да ну? попробуйте сделать sizeof() получившихся классов:
#include "stdafx.h"
 
class A{ virtual ~A(){} };
class B{ virtual ~B(){} };
class C{ virtual ~C(){} };
 
template< class T >
class I : public virtual T{};
 
template< class T1, class T2 >
class II : public virtual I< T1 >public virtual I< T2 >{};
 
template< class T1, class T2, class T3 >
class III : public virtual II< T1, T2 >public virtual II< T1, T3 >public virtual II< T2, T3 >{};
 
class X : public III<A,B,C>{};
 
class Y : public II<A,B>{};
 
int _tmain( int, _TCHAR*[] )
{
    cout << sizeof( X ) << endl; // 40
    cout << sizeof( Y ) << endl; // 20
    return 0;
}
 

40 (!) байт для пустого класса, наследуемого от трех интерфейсов
20 байт для пустого класса, наследуемого от двух интерфейсов

печалька :(

виртуальное наследование это такая фича, перед использованием которой надо очень хорошо подумать.
Благодаря этому объему, вызов виртуального метода через vtable дольше лишь на одну операцию JMP. Иначе был бы список (а не таблица) виртуальных методов и это было бы крайне медленно.

Да ладно, разве мешают эти несколько десятков байт, когда речь идёт об ООП?
> Да ладно, разве мешают эти несколько десятков байт, когда речь идёт об ООП?

Смотря как используются ваши объекты. Если объектов много, то могут мешать и очень сильно. Тоже самое возможно, если объекты маленькие, но часто копируются/создаются. Кроме того, если мне не изменяет память, наличие виртуального наследования влияет на размер указателя на функции-члены класса (хотя это надо проверять, точно не помню).

В добавок давайте посчитаем, сколько все-таки, ваш код требует памяти: (N+N!)*sizeof(void*) лишних байт на каждый экземпляр объекта (где N — количество интерфейсов). Для N=5 это уже 140 байт. А для N=7 более 5Кб…
Да… еще есть не очевидный минус виртуального наследования (особенно в таких количествах): dynamic_cast будет дико тормозить…
Неверно: виртуальное наследование предотвращает репликацию субобъекта виртуального базового класса для объектов наследующих классов, являющихся, в свою очередь, субобъектами базовых классов для объекта их общего класса-наследника.
Понятно. Размер объекта действительно будет быстро расти.
> когда речь идёт об ООП головного мозга?
Ничего личного. Просто я, например, до сих пор не вижу смысла во всем описанном в статье.
Как обычно бывает на хабре, статья содержит 20-30% полезной информации. Остальные 70-80% располагаются в комментариях. В статье я попытался решить возникшую передо мной проблему своими руками, в комментариях мне показали какие механизмы уже существуют в различных языках программирования.
Жаль мой заряд на сегодня иссяк.
В любом случае +1 за шутку)) раньше не встречал такой
2 альтернативных способа, которые делают тоже самое:

Первый, простой как дверь:
// Реализация этого выносится в .cpp а также может быть экспортирована
void DoFlyQuackingImpl( IQuackable *pQuackable, IFlyable *pFlyable )
{
    pFlyable->Fly();
    pQuackable->Quack();
}
 
// Реализация этого остается в .h, одна строчка это не сильно много
template< class T >
void DoFlyQuacking( T *)
{
    return DoFlyQuackingImpl( p, p );
};

Недостатки: для каждого кваколетанья надо писать враппер, хоть и тривиальный.

Второй, немного более сложный, но тоже простой как дверь:
class NA1 {};
class NA2 {};
class NA3 {};
class NA4 {};
 
template< class T1 = NA1, class T2 = NA2, class T3 = NA3, class T4 = NA4 >
class IGroup
{
    class CTrampoline : public T1, public T2, public T3, public T4 {};
public:
    typedef CTrampoline type;
};
 
class IQuackable { /*...*/ };
class IFlyable { /*...*/ };
 
class CDuck : public IGroup< IQuackable, IFlyable >::type {};
 
void DoFlyQuacking( IGroup< IQuackable, IFlyable >::type *)
{
    // ...
}

Недостатки: Очень плохо с вызовами нестандартных конструкторов IQuackable и/или IFlyable. Теоретически лечится с помощь varargs, но криво и некрасиво. Кроме того не подходит, если хотя бы один из интерфейсов цепляется к абстрактной утке на несколько абстракций выше…
Первый способ понятен, но внутри функции придётся пользоваться несколькими переменными, что может оказаться не так удобно. Но вариант вполне жизнеспособный.

Второй вариант не не будет работать при изменении порядка параметров. Впринципе это я и пытался сделать, только учел разный порядок аргументов — потому и вышло виртуальное наследование...
Второй вариант… да, завтыкал, сейчас подумаю как можно это исправить… т.е. есть вариант с шаблонной магией, но мне он не очень нравится…
Вот «правильный» второй вариант… стал намного сложнее, конечно, но никакого наследования :)

Суть в том, что CTrampoline генерируется для отсортированной последовательности типов T1..TN. У меня сортировка проводится по enum { eTypeID =… }, проверка на уникальность присутствует. В качестве eTypeID может быть, например ID строки с «человеческим» именем типа в ресурсах приложения. Определено оно должно быть для всех интерфейсов.

В принципе можно и по другому как-то сравнивать, тут полет фантазии.

#include "stdafx.h"
 
// .... Core ...
 
#include <iostream>
#include <tchar.h>
 
#include <boost/mpl/identity.hpp>
#include <boost/mpl/if.hpp>
#include <boost/mpl/less.hpp>
#include <boost/mpl/sort.hpp>
#include <boost/mpl/at.hpp>
#include <type_traits>
 
class NA1 { public: enum{ eTypeID = -1 }; };
class NA2 { public: enum{ eTypeID = -2 }; };
class NA3 { public: enum{ eTypeID = -3 }; };
class NA4 { public: enum{ eTypeID = -4 }; };
 
// Compares T1 and T2, default implementation really compares T1::eTypeID and T2::eTypeID
template< class T1, class T2 >
class Less
    : boost::mpl::less< boost::mpl::long_< T1::eTypeID >, boost::mpl::long_< T2::eTypeID > >
{
    static_assert( T1::eTypeID != T2::eTypeID || std::is_same< T1, T2 >::value"TypeID must be unique!" );
};
 
template<>
class Less< boost::mpl::_, boost::mpl::> {};
 
template< class T1, class T2, class T3, class T4 >
class CTrampoline : public T1, public T2, public T3, public T4 {};
 
template< class T1 = NA1, class T2 = NA2, class T3 = NA3, class T4 = NA4 >
class IGroup
{
    typedef typename boost::mpl::reverse_sort< boost::mpl::vector< T1, T2, T3, T4 >, Less< boost::mpl::_, boost::mpl::> >::type
        T;
public:
    typedef CTrampoline< typename boost::mpl::at_c< T, 0 >::type,
                         typename boost::mpl::at_c< T, 1 >::type,
                         typename boost::mpl::at_c< T, 2 >::type,
                         typename boost::mpl::at_c< T, 3 >::type >
        type;
};
 
// .... Usage ...
 
class IQuackable { public: enum{ eTypeID = 0 }; /*...*/ };
class IFlyable { public: enum{ eTypeID = 1 }; /*...*/ };
 
class CDuck : public IGroup< IQuackable, IFlyable >::type {};
 
void DoFlyQuacking( IGroup< IFlyable, IQuackable >::type *)
{
    // ...
}
 
int _tmain( int, _TCHAR*[] )
{
    CDuck oDuck;
 
    DoFlyQuacking( &oDuck );
 
    return 0;
}
 
Да, неплохая идея сортировать интерефейсы. Это уменишит количество базовых классов в факториал раз. Но всё же мне кажется этот метод не сработает, если класс будет наследован от A, B и C, а параметр будет типа IGroup<A,B>*. Щас попробую Ваш метод поюзать.
Да, не сработает… Но, думаю, можно что– то придумать и в этом направлении
Я просто изначально к этому и стремился. Попытаюсь на днях интерфейсы отсортировать в своём методе, если выйдет, допишу к статье как UPDATE. Ох не хочется мне лезть в boost::mpl…
Извращение уже почти достигло своего апогея, и тут я понял, что все просто:
#include "stdafx.h"
 
// .... Core ...
 
#include <iostream>
#include <tchar.h>
 
#include <boost/mpl/front.hpp>
#include <boost/mpl/pop_front.hpp>
#include <boost/mpl/identity.hpp>
#include <boost/mpl/remove_if.hpp>
#include <boost/mpl/vector.hpp>
#include <boost/mpl/transform.hpp>
#include <boost/mpl/empty.hpp>
#include <boost/utility/enable_if.hpp>
 
struct NA {};
 
//! Hierarchy generation core
template< class T1, class T2 >
struct Derive : T1, T2 {};
template< class T1 >
struct Derive< T1, NA > : T1 {};
 
template< class V, class EnableIf = void >
struct GenerateLinearHierarchy
    : boost::mpl::identity< Derive< 
        typename boost::mpl::front< V >::type,
        typename GenerateLinearHierarchy< typename boost::mpl::pop_front< V >::type >::type > >
{
};
 
template< class V >
struct GenerateLinearHierarchy< V, typename boost::enable_if< boost::mpl::empty< V > >::type >
    : boost::mpl::identity< NA >
{
};
 
//! Transforms interface type to proxy-to-interface type
template< class T >
struct InterfaceToProxyTransformOp : boost::mpl::identity< typename T::CProxy > {};
 
template<>
struct InterfaceToProxyTransformOp< boost::mpl::> {};
 
//! IGroup implementation, V is MPL array of interfaces.
template< class V >
struct IGroup_Impl
{
private:
 
    typedef typename boost::mpl::transform< V, InterfaceToProxyTransformOp< boost::mpl::> >::type
        TProxies;
 
    class CPointerImpl : public GenerateLinearHierarchy< TProxies >::type
    {
        template< class V, class T >
        typename boost::disable_if< boost::mpl::empty< V > >::type
        Initialize( T *)
        {
            static_cast< typename boost::mpl::front< V >::type * >( this )->SetTarget( p );
            Initialize< typename boost::mpl::pop_front< V >::type >( p );
        }
 
        template< class V, class T >
        typename boost::enable_if< boost::mpl::empty< V > >::type
        Initialize( T * ){}
 
    public:
 
        template< class T >
        CPointerImpl( T *){ Initialize< TProxies >( p ); };
    };
 
    class CPointer
    {
    public:
 
        template< class T >
        CPointer( T *pGroup ) : m_oImpl( pGroup ) {}
 
    public:
 
        CPointerImpl *operator->() { return &m_oImpl; }
        const CPointerImpl *operator->() const { return &m_oImpl; }
        const CPointerImpl &operator*() const { return m_oImpl; }
        CPointerImpl &operator*() { return m_oImpl; }
 
    protected:
 
        CPointerImpl m_oImpl;
    };
 
public:
 
    typedef CPointer
        TPointer;
};
 
template
<
    class T1 = NA, class T2 = NA, class T3 = NA,
    class T4 = NA, class T5 = NA, class T6 = NA,
    class T7 = NA, class T8 = NA, class T9 = NA
>
struct IGroup
    : IGroup_Impl<
            typename boost::mpl::remove_if<
                boost::mpl::vector< T1, T2, T3, T4, T5, T6, T7, T8, T9 >,
                std::is_same< boost::mpl::_, NA > >::type >
{
};
 
template< class T >
class TProxy
{
public:
    TProxy() : m_pTarget( nullptr ) {}
    void SetTarget( T *pTarget ){ m_pTarget = pTarget; }
protected:
    T *m_pTarget;
};
 
// .... Usage ...
 
class IQuackable
{
public:
    struct CProxy : TProxy< IQuackable >
    {
        void Quack(){ m_pTarget->Quack(); }
    };
public:
    virtual void Quack() = 0;
};
 
class IFlyable
{
public:
    struct CProxy : TProxy< IFlyable >
    {
        void Fly(){ m_pTarget->Fly(); }
    };
public:
    virtual void Fly() = 0;
};
 
class IWalkable
{
public:
    struct CProxy : TProxy< IWalkable >
    {
        void Walk(){ m_pTarget->Walk(); }
    };
public:
    virtual void Walk() = 0;
};
 
class CDuck : public IQuackable, public IFlyable, public IWalkable
{
public:
    virtual void Quack(){ std::cout << "Quack!" << std::endl; }
    virtual void Fly(){ std::cout << "Fly!" << std::endl; }
    virtual void Walk(){ std::cout << "Walk!" << std::endl; }
};
 
void DoFlyQuacking( IGroup< IFlyable, IQuackable >::TPointer p ){ p->Fly(); p->Quack(); }
void DoFlyWalkQuacking( IGroup< IFlyable, IWalkable, IQuackable >::TPointer p ){ p->Fly(); p->Walk(); p->Quack(); }
void DoFly( IGroup< IFlyable >::TPointer p ){ p->Fly(); }
 
int _tmain( int, _TCHAR*[] )
{
    CDuck oDuck;
 
    DoFlyQuacking( &oDuck );
    DoFlyWalkQuacking( &oDuck );
    DoFly( &oDuck );
 
    return 0;
}
 

В отсутствии виртуального наследования без proxy объектов сделать ничего невозможно чисто теоретически.
После Вашего слова «просто» всё уж как-то совсем не просто)
Если я и раскурю когда-нибудь этот код — буду собой гордиться))
Но если честно, от такого кода становится просто страшно...
Ну идея достаточно простая: пишем классы как обычно, но для всех интерфейсов определяем внутренний прокси класс, который переадресует все вызовы связанной с ним реализацией интерфейса. Когда нам надо вызвать функцию для объекта, который должен реализовывать определенный набор интерфейсов, мы определяем специальный тип «указателя», конструктор которого пытается инициализировать набор прокси объектов указателем на переданный объект; это компилируется только в том случае, когда объект действительно реализует все указанные интерфейсы.минусы: 1. для каждого интерфейса необходимо реализовать соответствующий прокси тип; 2. «указатель», которыми пользуются «DoQuack» функции является на самом деле составным объектом, который состоит из N обычных указателей (по одному на каждый тип интерфейса), но старательно делает вид, что он самый обычный (по другому нельзя, т.к. не существует полного типа для каждой комбинации интерфейсов)
Весь mpl в этом коде нужен только для того, чтобы не писать отдельные специализацию для разных количеств аргументов
К вопросу о примерах такой конструкции в других языках. Objective C:
- (void)someMethod:(id <UIAlertViewDelegate, UITableViewDataSource, BaseTVCProtocol>)obj;

Метод, принимающий агрумент любого типа реализующего 3 интерфейса:UIAlertViewDelegate, UITableViewDataSource, BaseTVCProtocol.
Спасибо за пример. Вот язык, что делали на совесть!
А никто не считал размер получаемого класса при группировке 3-х интерфейсов? Мне кажется, что оверхед будет довольно ощутимым из-за большого количества виртуальных наследников. А если еще добавить виртуальность к классам, например, ввести виртуальный деструктор, то получится еще больше.
Если я правильно понял чего хотел добиться автор, то это весьма близко к идее концептов в С++.

Основное отличие в том, что концепты предназначены для проверки параметров шаблонов, а не функций, что требует делать шаблонными все функции, принимающие такие параметры (что в принципе и без концептов можно наблюдать в STL), но избавляет от вызовов виртуальных функций.

Если скоростные вызовы методов классов не нужны, последнее ограничение можно было бы попробовать обойти с помощью type erasure.

P.S.
Помимо Boost Concept Check есть ещё другая библиотека (насколько я понял, рассчитанная на C++0x), реализующая идею концептов. Вот слайды её презентации с BoostCon 2011: https://github.com/boostcon/2011_presentations/raw/master/thu/Boost.Generic.pdf

P.P.S.
Раз уж я упомянул STL грех не вспомнить про tag dispatching и другие техники обобщенного программрования в C++ (главное ими чрезмерно не увлекаться).
Этот пример похож на пример из хорошей книги по Шаблонам Проектирования Head First design patterns (O'Reilly Media, Inc., 2004). Очень хорошая книжка для начального понимания шаблонов проектирования. Книга только на английском.
Sign up to leave a comment.

Articles