Массив функций с разными сигнатурами

У меня есть эти классы:

class Foo
{
    ...
};

class Foo1 : public Foo
{
    ...
};

...

class FooN : public Foo
{
    ...
};

Возможно ли иметь массив функций с такими сигнатурами:

void f1(Foo1*){}
...
void fN(FooN*){}

Есть ли какие-либо изменения, если эти функции не являются статическими функциями-членами, а не обычными функциями? Я не думаю, что это что-то изменит.

Спасибо!


person Mircea Ispas    schedule 07.09.2011    source источник
comment
Можно ли использовать полиморфизм вместе с указателем на Foo? У вас не может быть массива указателей на функции с разными сигнатурами. Вы можете либо сделать так, чтобы у всех был Foo*, либо у всех был void* (я предлагаю Foo*).   -  person Corbin    schedule 07.09.2011
comment
Если у меня будет указатель на Foo*, мне нужно будет сделать приведение к FooK внутри fK, чего я хочу избежать.   -  person Mircea Ispas    schedule 07.09.2011
comment
Вам придется либо найти способ сделать это полиморфно (поскольку spraff подробно остановился на моем первом комментарии), либо повеселиться приведением указателя.   -  person Corbin    schedule 07.09.2011
comment
У вас несовместимые цели. Если вы хотите избежать dynamic_cast вас, вам нужно не помещать разные типы функций в один и тот же контейнер. Разделите эти типы в своем дизайне.   -  person spraff    schedule 07.09.2011
comment
Другой способ сказать, что все в массиве должно быть одного типа, поэтому вам нужно решить и указать, какие аспекты типов FooN одинаковы.   -  person spraff    schedule 07.09.2011
comment
Что вы собираетесь делать с таким массивом? Массивы обычно используются для однородного доступа к элементам. Однако у вас нет информации во время выполнения о конкретной сигнатуре функций, упакованных в массив, и, следовательно, вы даже не можете правильно вызывать функции f[0](..), f[1](..) , как вы это делаете. не знаю тип аргумента. Или у вас есть дополнительная информация, которая отсутствует в вашем вопросе?   -  person user396672    schedule 07.09.2011
comment
@iammilind Почему вы удалили тег? Возможно, это функция нового стандарта, которая делает это возможным или делает возможным трюк для этого.   -  person Mircea Ispas    schedule 07.09.2011
comment
Такой новой функции нет, и тег не подходит. Теги с категориями, которые, по вашему мнению, могут волшебным образом содержать ответ, вредят сайту — теги предназначены для категоризации вопросов.   -  person spraff    schedule 07.09.2011


Ответы (10)


РЕДАКТИРОВАТЬ альтернативное решение, не основанное на виртуальных функциях, здесь.

Тип void(*)(Foo*) не может быть преобразован в тип void(*)(Bar*) и не зря.

Вы должны сделать так, чтобы все ваши функции принимали аргумент Interface*, а все FooN должны быть производными от Interface.

struct Interface {
    virtual ~ Interface () {}
    // ...
};

struct Foo1 : public Interface {
    // ...
};

struct Foo2 : public Interface {
    // ...
};

void f1 (Interface *);
void f2 (Interface *);

void (*functions)(Interface*) [] = {f1, f2};

functions[0] (new Foo1 ());
functions[0] (new Foo2 ());
functions[1] (new Foo1 ());
functions[1] (new Foo2 ());

Реализации f1, f2 могут во время выполнения проверять, является ли их аргумент конкретной реализацией, используя dynamic_cast и проверяя наличие nullptr. Единственный способ проверить во время компиляции — заставить f1 и f2 принимать определенные типы и не помещать их в анонимный массив, а вызывать их явно.


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

person spraff    schedule 07.09.2011
comment
Я знаю, что могу создать массив функций с одинаковой сигнатурой, я хочу знать, возможно ли создать массив функций с разными сигнатурами, используя какой-то трюк или что-то в этом роде :). Просто замените интерфейс базовым Foo, и у вас будет тот же код, что и в моем вопросе :) - person Mircea Ispas; 07.09.2011
comment
Замените void f(Foo1*) на void f(Foo*), и вы получите мой ответ :-) Массивы должны содержать объекты одного типа. Если вы можете доказать, что все вызовы будут содержать определенные типы среды выполнения, то разумнее всего будет иметь один массив для каждого конкретного типа. - person spraff; 07.09.2011

Вы можете использовать функциональные объекты. Посмотрите пример ниже, как сделать это самостоятельно. Если вам нравится эта идея, вы должны взглянуть на boost.signal/boost.bind и аналоги c++ 0x.

class Foo1 {};
class Foo2 {};
class Foo3 {};

void func1(Foo1*) {}
void func2(Foo2*) {}
void func3(Foo3*) {}

class FuncObjBase {
public:
    virtual void operator()() = 0;
};

template <class T>
class FuncObj : public FuncObjBase {
public:
    typedef void (*Funcptr)(T*);
    FuncObj(T* instance, Funcptr funcptr) : m_Instance(instance), m_Func(funcptr) {}
    virtual void operator()() { m_Func(m_Instance); }
private:
   T* m_Instance;
   Funcptr m_Func;
};

int main(int argc, char *argv[])
{
    Foo1 foo1;
    Foo2 foo2;
    Foo3 foo3;
    FuncObjBase* functions[3];
    functions[0] = new FuncObj<Foo1>(&foo1, func1);
    functions[1] = new FuncObj<Foo2>(&foo2, func2);
    functions[2] = new FuncObj<Foo3>(&foo3, func3);
    for(unsigned int i = 0; i < 3; i++) {
        (*functions[i])();
    }
    return 0;
}
person David Feurle    schedule 07.09.2011
comment
Хотя все они благополучно помещаются в массив, вы больше не можете вызывать ни одну из функций с аргументами, отличными от тех, с которыми они FuncObj связаны. -1 за упущение сути, повторную реализацию function/bind, даже если вы знали о них, и за использование в примере очень хитроумной логики владения объектами. - person spraff; 08.09.2011
comment
Дело точно не в баллах. Большинство ответов здесь небезопасны для типов, и меня огорчает, что людям, похоже, все равно. Это безопасно для типов, но бесполезно, потому что требует привязки аргумента. Между прочим, умные указатели не имеют ничего общего с проблемами владения. - person spraff; 08.09.2011

C++ — это язык со статической типизацией, включая типы функций. В каждой строке кода компилятор C++ должен определить, допустима ли сигнатура функции и какую функцию (или указатель) вызывать.

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

Лучшее, что вы можете сделать, это использовать что-то вроде массива boost::variant. У вас может быть определенный набор прототипов функций, хранящихся в варианте, возможно, с использованием файла boost::function. Однако это будет только ограниченный набор, а не произвольный тип функции. И вызвать их было бы довольно сложно, так как вам сначала нужно было бы убедиться, что вариант действительно относится к ожидаемому типу функции, а затем вызвать его.

Другой альтернативой является использование массива boost::any. За исключением этого, типы могут быть любым типом функции. Опять же, для ее вызова потребуется преобразовать ее в один из ожидаемых типов функций. Проблема усугубляется тем, что типы функций могут быть буквально любыми. Поэтому вам придется предоставить запасной вариант, если это не один из ожидаемых типов функций.

Если список функций невелик и определяется временем компиляции, вы можете использовать boost::tuple в качестве импровизированного "массива". Однако вы должны использовать метапрограммирование шаблонов, чтобы перебирать их. Конечно, если бы это было так, вы могли бы просто использовать структуру, содержащую указатели функций соответствующего типа.

person Nicol Bolas    schedule 07.09.2011
comment
Есть ли какая-то причина для -1? - person Nicol Bolas; 07.09.2011

Вы можете сделать это в С++ 11 с помощью шаблонов Variadic. Проверьте мой ответ, который похож на то, что вы хотите, но с картами по адресу: https://stackoverflow.com/a/33837343/1496826

person Mohit    schedule 20.11.2015

Вы можете использовать функциональные объекты.

Например Boost.Signal или из C++0x/TR1

person David Feurle    schedule 07.09.2011
comment
Нет, полный тип std::function, например, std::function<void(Foo*)>. Эта полиморфная функция-оболочка вполне справедливо имеет точное соответствие типов с эквивалентными обычными функциями. - person spraff; 07.09.2011
comment
@spraff это делает их идеальными для использования в массиве функциональных объектов или? - person David Feurle; 07.09.2011
comment
Вы не можете использовать std::funcion<void(Foo*)> в том же массиве, что и std::funcion<void(Bar*)>, потому что это объекты разных типов. - person spraff; 07.09.2011
comment
@spraff с std::bind вы можете - person David Feurle; 07.09.2011
comment
Я думаю, вы неправильно понимаете, для чего нужен std::bind. Я не думаю, что он может отображать Foo* в Bar* таким образом (он привязывает конкретный Foo* или Bar* к функции, получая std::function<void()>), но даже если бы он мог сделать что это не то, что требуется. - person spraff; 08.09.2011
comment
@spraff Я думал, что это то, что нужно - › поместить функции в массив. Не требовалось, чтобы массив был какого-то определенного типа, например std::function‹void(Foo1*)› - person David Feurle; 08.09.2011

Вы можете сделать функции с f1 по fN членами своих конкретных классов аргументов, назвать их одинаковыми и использовать виртуальную диспетчеризацию для вызова правильных функций. Тогда вам просто нужно заполнить указатели на функции-члены в массиве.

person arne    schedule 07.09.2011

Я нашел этот обходной путь для этой проблемы:

#include <iostream>
#include <vector>

class Foo
{
};

class Foo1 : public Foo
{
};

class Foo2 : public Foo
{
};

class Foo3 : public Foo
{
};


void f1(Foo1*)
{
    std::cout<<"f1\n";
}

void f2(Foo2*)
{
    std::cout<<"f2\n";
}

void f3(Foo3*)
{
    std::cout<<"f3\n";
}

template<typename T>
void AddPointer(std::vector<typename void (*)(Foo*)>& fPointers, T function)
{
    fPointers.push_back(reinterpret_cast<void (*)(Foo*)>(function));
}

void main()
{
    std::vector<typename void (*)(Foo*)> fPointers;

    AddPointer(fPointers, f1);
    AddPointer(fPointers, f2);
    AddPointer(fPointers, f3);

    Foo1 foo1;
    Foo2 foo2;
    Foo3 foo3;

    fPointers[0](&foo1);
    fPointers[1](&foo2);
    fPointers[2](&foo3);
}
person Mircea Ispas    schedule 07.09.2011
comment
Вы без нужды отбрасываете безопасность типов. Для этого есть типобезопасные способы без накладных расходов. - person spraff; 07.09.2011
comment
@spraff Мне не нужна безопасность типов, мне просто нужно иметь возможность вызывать функции с другой сигнатурой из вектора без какого-либо приведения внутри функций. Это мой вопрос, это ответ на него. Если у меня уже есть информация о типе вне вектора, зачем делать еще одно приведение внутри функции? Эта реализация находится внутри библиотеки, ее пользователь должен знать только то, что если он зарегистрирует функцию с сигнатурой внутри менеджера, то эта функция будет вызываться с правильным параметром и ему не нужно ее приводить. Библиотека должна иметь простой интерфейс, потому что этого хотят пользователи. - person Mircea Ispas; 07.09.2011
comment
Вам нужна безопасность типов. Когда вы регистрируете функцию с определенной сигнатурой в менеджере, менеджер должен где-то хранить эту функцию. Когда вы регистрируете функцию с другой сигнатурой в менеджере, менеджер должен хранить эту функцию где-то еще, а не в том же массиве/векторе. - person spraff; 08.09.2011

Я бы предложил использовать std::tuple вместо std::array или C-массива. Используя std::tuple, вы можете хранить элементы разных типов.

person Clinton    schedule 08.09.2011

Вот общий подход, который является типобезопасным и заставляет клиентский код быть правильным.

class Manager {
public:

    typedef int /* or whatever */ ID;

    template <typename Function>
    static void save (Function * f, ID id) {
        functions <Function> () .add (id, f);
    }

    template <typename Function>
    static Function * get (ID id) {
        return functions <Function> () .get (id);
    }

private:

    template <typename Function>
    class FunctionStore {
    public:

         void add (Function *, ID);
         Function * get (ID);

    private:
         // or vector, if you know ID is int.
         std :: map <ID, Function *> m_functions;
    };

    // type_index is C++11 but you can implement it in C++03.
    // void* here is unpleasant but you can improve it, RAII it.
    typedef std :: map <std :: type_index, void *> Store;
    static Store m_store;

    template <typename Function>
    FunctionStore <Function> & functions () {
        FunctionStore <Function> * fs;

        Store :: iterator i = m_store .find (typeid Function);

        if (m_store .end () == i) {
            fs = new FunctionStore <Function> ();
            m_store [typeid Function] = fs;
        }
        else {
            // This void* cast is OK because it's internally controlled
            // and provably correct.
            // We don't have to trust the library to not abuse it.
            fs = static_cast <FunctionStore<Function>*> (i -> second);
        }

        return *fs;
    }
};

// In the library

void foo1 (Foo *);
void bar1 (Bar *);
void foo2 (Foo *);
void bar2 (Bar *);

void init () {
    Manager :: save (foo1, 1);
    Manager :: save (foo2, 2);
    Manager :: save (bar1, 1);
    Manager :: save (bar2, 2);

    Manager :: get <void(Foo*)> (1) (new Foo ()); // OK, calls foo1
    Manager :: get <void(Foo*)> (1) (new Bar ()); // Will not compile
    Manager :: get <void(Bar*)> (2) (new Bar ()); // OK, calls bar2
}

Если вы не хотите накладных расходов на поиск в m_store (и/или хотите избежать void в Manager::Store), вы можете сделать Manager самим классом шаблона, недостатком является то, что теперь вам нужно следить за вашими статическими определениями m_store. Это нормально, если вы знаете, что клиенты будут использовать только заданный набор Function подписей.

void init () {
    Manager <void(Foo*)> :: save (foo1, 1);
    Manager <void(Foo*)> :: save (foo2, 2);
    Manager <void(Foo*)> :: save (bar1, 1); // Won't compile
    Manager <void(Bar*)> :: save (bar1, 1);
    Manager <void(Bar*)> :: save (bar2, 2);

    Manager <void(Foo*)> :: get (1) (new Foo ()); // OK, calls foo1
    Manager <void(Foo*)> :: get (1) (new Bar ()); // Will not compile
    Manager <void(Bar*)> :: get (2) (new Bar ()); // OK, calls bar2
}

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

person spraff    schedule 08.09.2011

Вам нужны ковариантные типы аргументов. Это не поддерживается в C++, поскольку нарушает безопасность типов. Чтобы лучше понять это, давайте возьмем простой пример:

struct Vehicle {};
struct Tricycle : Vehicle {};
struct Tank : Vehicle {};

void drive(Vehicle const & b) { ... }
void giveToChild(Tricycle const & b) { ... }

Здесь у нас есть простая иерархия типов, а также две функции, которые ссылаются соответственно на базовый класс и один из производных классов. Теперь, если бы то, о чем вы просите, было разрешено, мы могли бы сделать следующее:

typedef void (*funcPtr)(Vehicle const &);

funcPtr = &giveToChild; // this is not allowed
funcPtr(Tank());        // oops, I just gave a tank to my child!

Язык мог бы реализовать какую-то проверку типа во время выполнения, но C++ работает иначе.

Однако обратное преобразование (контравариантность) может быть разрешено без каких-либо проблем (на самом деле делегаты C# разрешают это), но невозможно в C++ по некоторым причинам, о которых я не знаю. Вот пример того, что это позволит:

typedef void (*funcPtr)(Tricycle const &);

funcPtr = &drive;    // this could be allowed, but is not (in C++)
funcPtr(Tricycle()); // I can only drive a tricycle, but that's ok since it's a
                     // vehicle and I know how to drive all vehicles

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

void forwardFN(Foo * f)
{
    FooN * instance = dynamic_cast<FooN *>(f);

    if (instance) fN(instance);
    else throw type_exception();
}
person Luc Touraille    schedule 07.09.2011