Как мне написать функцию, которая принимает аргументы разных типов с одинаковым интерфейсом? - PullRequest
0 голосов
/ 23 марта 2011

Рассмотрим этот упрощенный пример:

#include <list>

typedef std::list<int> IntList;

class KindaIntList {
    public:
        IntList::const_iterator begin() const { /* do some stuff */ }
        IntList::const_iterator end() const { /* do some stuff */ }
        // ...etc
};

Класс KindaIntList реализует некоторые методы STL list.

Теперь у меня есть функция

void f(IntList l) {
    // do stuff
}

, который вызывает только те методы, которые реализованы KindaIntList.Я хотел бы иметь возможность вызывать его с IntList или с аргументом KindaIntList.Это возможно?

Я думал об использовании шаблонов, но определение f довольно большое, и я не хочу помещать его в заголовочный файл (f является членом класса,и я не хочу, чтобы он был встроен).

Редактировать

Функция f на самом деле является virtual членом другого класса;поэтому я не уверен, как превратить его в элемент шаблона.

Ответы [ 2 ]

4 голосов
/ 23 марта 2011

Несмотря на ваши опасения по поводу шаблонов, это действительно подходящее место для использования шаблонов C ++.Шаблонные функции прекрасно отражают идею «эта функция работает с любыми аргументами, если операции, которые я выполняю над этими аргументами, четко определены».

Вам не нужно беспокоиться о вставке в этом случае.Если вы не определите f внутри тела класса, оно не будет автоматически вставлено, даже если это шаблон.Например, в этом коде:

class MyClass {
public:
     template <typename T> void f(T&);
};

template <typename T> void MyClass::f(T&) {
    /* ... implementation ... */
}

Поскольку f не определено внутри тела MyClass, оно не считается встроенной функцией.

Что касается вашего беспокойства о том, чтобы сделать заголовочный файл слишком большим, я утверждаю, что это действительно не о чем беспокоитьсяЕсли вы беспокоитесь о том, чтобы сделать заголовок слишком большим, вы можете поместить большой комментарий на полпути, говоря что-то вроде

 /* * * * * Implementation Below This Point * * * * */

В качестве альтернативы, вы можете сделать отдельный файл .h для реализации шаблона,затем #include этот файл внизу заголовочного файла.Это защищает клиента от просмотра реализаций шаблонов, если они активно его не ищут.

Надеюсь, это поможет!

РЕДАКТИРОВАТЬ: Если f виртуальный, то выне может сделать это функцией шаблона (как вы, наверное, поняли).Следовательно, если вы хотите, чтобы это работало для «вещей, которые выглядят как std::list», то у вас не так много хороших вариантов.Обычно вы создаете базовый класс как для std::list, так и для своего пользовательского типа списка, но это не вариант, так как вы не можете изменить std::list.

К счастью, есть способ леченияstd::list и вещи, которые похожи на это полиморфно, используя трюк под названием внешний полиморфизм .Идея состоит в том, что хотя вы не можете заставить соответствующие классы вести себя полиморфно, вы можете добавить дополнительный слой косвенности вокруг этих объектов, введя полиморфную иерархию классов, которая просто перенаправляет все свои запросы к объектам, которые сами по себе не являются полиморфными.

Если вы хотите вывести Big Template Guns, вы можете инкапсулировать эту логику внутри класса, который работает почти так же, как новый тип шаблона std::function.Идея заключается в следующем.Сначала мы создадим полиморфный базовый класс, который экспортирует все функции, которые вы хотите вызвать, как чисто виртуальные функции:

class List {
public:
    virtual ~List() {}

    virtual std::list<int>::const_iterator begin() const = 0;
    virtual std::list<int>::const_iterator end() const = 0;

    virtual void push_back(int value) = 0;

    /* ... etc. ... */
};

Теперь мы можем определить template подкласс List который реализует весь открытый интерфейс, перенаправляя все вызовы объекту фактического типа.Например:

template <typename T> class ListImpl: public List {
private:
    T& mImpl; // Actual object that does the work
public:
    /* Constructor stores a reference to the object that actually does the work. */
    ListImpl(T& impl) : mImpl(impl) {
         // Handled in initializer list
    }

    /* These functions all forward the requests to the implementation object. */
    virtual std::list<int>::const_iterator begin() const {
         return mImpl.begin();
    }
    virtual std::list<int>::const_iterator end() const {
         return mImpl.end();
    }
    virtual void push_back(int value) {
         mImpl.push_back(value);
    }

    /* ... etc. ... */
};

Теперь, когда у вас есть эта оболочка, вы можете реализовать f так, чтобы она принимала List:

class MyClass {
public:
    void f(List* myList) {
        myList->push_back(137); // For example
    }
};

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

MyClass mc;
std::list<int> myList;
MyIntList myIntList;

mc->f(new ListImpl<std::list<int> >(myList));
mc->f(new ListImpl<MyIntList>(myIntList));

Конечно, это громоздко и громоздко.Вы также должны беспокоиться об утечке ресурсов, что не очень весело.К счастью, вы можете решить эту проблему, свернув всю логику для работы с List и ListImpl во вспомогательном классе, как показано здесь:

class ListWrapper {
public:
    template <typename ListType> ListWrapper(ListType& list) {
        /* Store a wrapper of the appropriate type. */
        mImpl = new ListImpl<ListType>(list);
    }

    /* Delete the associated implementation object. */
    ~ListWrapper() {
        delete mImpl;
    }

    /* For each interface function, provide our own wrapper to forward the logic
     * to the real implementation object.
     */
    std::list<int>::const_iterator begin() const {
        return mImpl->begin();
    }
    std::list<int>::const_iterator end() const {
        return mImpl->end();
    }
    void push_back(int value) {
        mImpl->push_back(value);
    }

    /* ... etc. ... */

    /* Copy functions necessary to avoid serious memory issues. */
    ListWrapper(const ListWrapper& rhs) {
        mImpl = rhs.mImpl->clone();
    }
    ListWrapper& operator= (const ListWrapper& rhs) {
        if (this != &rhs) {
             delete mImpl;
             mImpl = rhs.mImpl->clone();
        }
        return *this;
    }

private:
    List* mImpl; // Pointer to polymorphic wrapper
};

Теперь вы можете написать f, чтобы взятьв ListWrapper вот так:

class MyClass {
public:
    virtual void f(ListWrapper list) {
        list.push_back(137); // For example
    }
};

(Предполагается, что вы обновили List и ListImpl с помощью виртуальной функции clone, которая делает копию объекта, которую я 'для краткости опущено).

И магически этот код теперь допустим (и безопасен!):

MyClass mc;
std::list<int> myList;
MyIntList myIntList;

mc.f(myList);
mc.f(myIntList);

Этот код работает, потому что конструктор шаблона для ListWrapper автоматически выведеттип его аргумента и неявно создать объект типа ListImpl, соответствующий этому объекту.Он также инкапсулирует управление памятью для вас, так что вы никогда не увидите явных new с или delete с.Более того, это означает, что вы можете передать любой объект, который вам нравится, и все будет работать автоматически - мы по существу сделали все, что выглядит как list полиморфное, используя параллельную иерархию классов!

Whew!Это было весело!Надеюсь, это поможет!

1 голос
/ 23 марта 2011

Вы можете перегрузить f, чтобы либо принять IntList и KindaIntList, например:

void f(IntList l){...}
void f(KindaIntList l){...}

, либо заставить его принимать итераторы:

void f(IntList::iterator first, IntList::iterator last){...}

Тем не менее, шаблоныдействительно лучший выбор здесь для обоих случаев:

template<class ListT>
void f(ListT l){...}
template<class Iter>
void f(Iter first, Iter last){...}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...