Вектор std::функция с разными сигнатурами

#c #c 11

#c #c 11 #stdvector #std-функция

Вопрос:

У меня есть несколько функций обратного вызова с разными сигнатурами. В идеале я хотел бы поместить их в вектор и вызвать соответствующий вектор в зависимости от определенных условий.

например ,

 void func1(const std::stringamp; value);

void func2(const std::stringamp; value, int min, int max);

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    func2,
};
 

Я понимаю, что вышесказанное невозможно, но мне интересно, есть ли какие-либо альтернативы, которые я должен рассмотреть. Я пока не смог найти ни одной, и я экспериментировал, std::bind но не смог добиться того, чего хочу.

Возможно ли такое?

Комментарии:

1. Почему бы просто не поставить им одинаковые подписи? Вам не обязательно использовать все параметры в функции

2. Зачем вам нужно использовать вектор для этого? Вектор может содержать только типы, которые имеют один и тот же тип (учитывается полиморфизм). Если у вас есть функции с разными сигнатурами, вы не сможете этого сделать, например for( autoamp;amp; function : functions){ function(); } , поэтому вам все равно нужно разделить их или унифицировать их сигнатуры (но если у них разные сигнатуры, этот последний вариант выглядит как грубое принуждение их к вектору …)

3. @EdHeal — Думаю, я мог бы. Просто задумался об альтернативах.

4. @JBL — В конечном счете, это то, что я хотел сделать. Я предполагаю, что вы говорите, что это невозможно.

5. Что именно ты хочешь сделать? Посмотрите на эти разные ответы.. Я проголосую за закрытие за «непонятно, о чем спрашиваю».

Ответ №1:

Вы не сказали, что вы ожидаете, что сможете сделать func2 после помещения его в вектор с неправильным типом.

Вы можете легко использовать std::bind его для помещения в вектор, если заранее знаете аргументы:

 const std::vector<std::function<void(std::string)>> functions
{
    func1,
    std::bind(func2, std::placeholders::_1, 5, 6)
};
 

Сейчас functions[1]("foo") позвонит func2("foo", 5, 6) , и передаст 5 и 6 func2 тому подобное.

Здесь то же самое, используя лямбда вместо std::bind

 const std::vector<std::function<void(std::string)>> functions
{
    func1,
    [=](const std::stringamp; s){ func2(s, func2_arg1, func2_arg2); }
};
 

Если вы еще не знаете аргументы, вы можете привязать ссылки к некоторым переменным:

 int func2_arg1 = 5;
int func2_arg2 = 6;
const std::vector<std::function<void(std::string)>> functions
{
    func1,
    std::bind(func2, std::placeholders::_1, std::ref(func2_arg1), std::ref(func2_arg2))
};
 

Теперь functions[1]("foo") будет вызываться func2("foo", func2_arg1, func2_arg2) , и вы можете присвоить новые значения целым числам для передачи разных аргументов func2 .

И использование лямбда-функции вместо std::bind

 const std::vector<std::function<void(std::string)>> functions
{
    func1,
    [amp;](const std::stringamp; s){ func2(s, func2_arg1, func2_arg2); }
};
 

Однако это довольно некрасиво, поскольку вам нужно сохранять int переменные до тех пор, пока вызываемый объект (замыкание или выражение привязки), ссылающийся на них, существует.

Комментарии:

1. Спасибо за ваш ответ. Использование std::bind так, как вы показали (поскольку я знаю аргументы во время компиляции), — это то, что я пробовал, когда сказал, что экспериментировал с std::bind, но не смог заставить его работать. Теперь я могу благодаря вашему ответу.

Ответ №2:

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

 #include <iostream>
#include <functional>
#include <memory>
#include <vector>

void foo(int) {
    std::cout << "I'm foo!n";
}

int bar(char, double) {
    std::cout << "I'm bar!n";
}

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

        virtual void operator()() = 0;
};

class MyFunctionA : public MyFunction {
    public:
        virtual void operator()() {
            foo(4);
        }
};

class MyFunctionB : public MyFunction {
    public:
        MyFunctionB(std::function<int(char,double)> f, char arg1, double arg2) : fun_(f), arg1_(arg1), arg2_(arg2) {} 

        virtual void operator()() {
            fun_(arg1_, arg2_);
        }
    private:
        std::function<int(char,double)> fun_;
        char arg1_;
        double arg2_;
};

int main() {
    using MyFunPtr = std::unique_ptr<MyFunction>;
    std::vector<MyFunPtr> v;

    v.emplace_back(new MyFunctionA());
    v.emplace_back(new MyFunctionB(bar, 'c', 3.4));

    for ( autoamp;amp; myfun : v ) {
        (*myfun)();
    }
    return 0;
}
 

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

Ответ №3:

Для C 17 std::variant может использоваться для хранения std::function s с разными сигнатурами. В этом случае функция std::holds_alternative позволяет вам различать их во время выполнения:

Пример:

 #include <variant>
#include <iostream>
#include <functional>
#include <vector>


using FooInt = std::function<void(int)>;
using FooStr = std::function<void(std::string)>;

using FooVariant = std::variant<FooInt, FooStr>;

void foo(int a){
  std::cout << a << std::endl;
}

void bar(std::string a){
  std::cout << a << std::endl;
}

int main()
{
  std::vector<FooVariant> v;
  v.push_back(foo);
  v.push_back(bar);

  for(autoamp; f: v){
    if (std::holds_alternative<FooInt>(f))
      std::get<FooInt>(f)(1);
    else if (std::holds_alternative<FooStr>(f))
      std::get<FooStr>(f)("hello");
  }
}
 

Ответ №4:

Прямой ответ на ваш вопрос «НЕТ». Любой контейнер среды выполнения позволит вам хранить объекты только одного типа, а std::function<>, созданные с разными сигнатурами, будут иметь разные типы данных.

Как правило, причина, по которой вы можете захотеть иметь «вектор функций с разными сигнатурами», заключается в том, что у вас есть что-то вроде приведенного ниже (трехэтапная обработка, при которой входной интерфейс унифицирован ( bufferamp; buf и выходной интерфейс унифицирован on_event(Event evt) ), но слой в середине неоднороден process_...(...)

 receive_message(bufferamp; buf)
  switch(msg_type(buf))
    case A: 
    case B:
    ...

process_A(Aamp; a, One x, Two y)
  ...
  dispatch(Event evt);
  ...

process_B(Bamp; b, Three x);
  ...
  dispatch(Event evt);
  ...
 

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

 vector <std::function<void(bufferamp; buf)>> handlers;
 

Ответ №5:

Если у вас есть int и string , вы не можете поместить их в один вектор, но вы можете поместить их в одну структуру или std::tuple<> . То же самое относится к двум типам функций.

Комментарии:

1. это не вопрос

2. @BeyelerStudios: он явно запрашивает альтернативу vector<> для хранения двух функций. Не tuple<> способен ли это сделать?

3. под «количеством функций обратного вызова» я понимаю, что число является произвольным во время выполнения, кортежи здесь не подойдут

4. Даже тогда вполне вероятно, что набор довольно ограничен, и вы просто установите функции, которые вам не нужны nullptr .

Ответ №6:

std::function стирает точный тип объекта функции, но сохраняет сигнатуру вызова функции. Если вы не можете bind заранее указать дополнительные аргументы, как рекомендует Джонатан Уэйкли, вы можете использовать a boost::variant< std::function<...>, std::function<...> > в качестве элемента вектора. Затем на сайте вызова вы можете проверить, содержит ли вектор объект функции правильного типа, и вызвать его соответствующим образом.

Комментарии:

1. Спасибо. Я смог получить то, что хотел, используя std::bind, следуя ответу Джонатана Уэйкли.

Ответ №7:

Не уверен, насколько это было бы полезно для вас, оно основано на boost::any том, что избыточные параметры игнорируются. Вы можете добавить try...catch for boost::bad_any_cast , чтобы предотвратить сбой в случае несоответствия между типами аргументов и параметров. Хотя я думаю, что обычный std::bind — лучший выбор.

ДЕМОНСТРАЦИЯ

 #include <boost/any.hpp>
#include <functional>
#include <vector>
#include <cstddef>
#include <memory>
#include <tuple>
#include <utility>
#include <iostream>
#include <string>

struct IGenericFunction
{
    virtual ~IGenericFunction() = default;

    virtual void call(boost::any a1 = boost::any{}
                    , boost::any a2 = boost::any{}
                    , boost::any a3 = boost::any{}
                    , boost::any a4 = boost::any{}) = 0;
};

template <typename... Args>
class GenericFunction : public IGenericFunction
{
public:
    GenericFunction(std::function<void(Args...)> f) : _f{ f } {}

    virtual void call(boost::any a1 = boost::any{}
                    , boost::any a2 = boost::any{}
                    , boost::any a3 = boost::any{}
                    , boost::any a4 = boost::any{}) override
    {
        call_func(std::make_tuple(a1, a2, a3, a4)
                , std::make_index_sequence<sizeof...(Args)>{});
    }

private:            
    template <typename Tuple, std::size_t... Indices>
    void call_func(Tuple t, std::index_sequence<Indices...> s)
    {
        _f(boost::any_cast<
                typename std::tuple_element<Indices, Params>::type
           >(std::get<Indices>(t))...);
    }

    std::function<void(Args...)> _f;

    using Params = std::tuple<Args...>;
};

template <typename... Args>
std::shared_ptr<IGenericFunction> make_generic_function_ptr(void(*f)(Args...))
{
    return std::make_shared<GenericFunction<Args...>>(f);
}

void func1(const std::stringamp; value)
{
    std::cout << "func1 " << value << std::endl;
}

void func2(const std::stringamp; value, int min, int max)
{
    std::cout << "func2 " << value << " " << min << " " << max << std::endl;
}

int main()
{
    std::vector<std::shared_ptr<IGenericFunction>> functions;

    functions.push_back(make_generic_function_ptr(amp;func1));    
    functions.push_back(make_generic_function_ptr(amp;func2));

    for (auto f : functions)
    {
        f->call(std::string("abc"), 1, 2);
    }
}
 

Ответ №8:

Как упоминал JBL: как бы вы их назвали, если вы не знаете их сигнатур?

Подумайте о том, чтобы превратить ваши min, max аргументы в тип параметра с некоторым базовым классом Parameter , сигнатурой обратного вызова будет void(const std::stringamp;, const Parameteramp;) или void(const std::stringamp;, const Parameter*) , если вы не хотите nullptr указывать дополнительные параметры. Теперь вашим обратным вызовам необходимо будет проверить, были ли им заданы правильные параметры, если таковые имеются. Это может быть сделано с помощью visitor, typeid или enum . У всего этого есть свои плюсы и минусы.

Как вы решите, какой обратный вызов вызывать? Я думаю, вам следует превратить ваши обратные вызовы в стиле C в объекты-обработчики, они могут реализовать функцию bool canHandle(const Parameteramp;) для проверки, применим ли обработчик к представленным параметрам.

Джонатан Уэйкли и Свалорцен представляют свой подход, в котором параметры и функция являются одним и тем же объектом (отношение 1 к 1). В этом примере они являются отдельными (в случае, если у вас есть отношения «несколько к нескольким»):

 #include <cassert>
#include <string>
#include <typeinfo>
#include <vector>

class ParameterBase {
public:
    ParameterBase(const std::stringamp; value) : m_value(value) { }
    virtual ~ParameterBase() { }
    const std::stringamp; GetValue() const { return m_value; }
private:
    std::string m_value;
};

class HandlerBase {
public:
    virtual bool CanHandle(const ParameterBaseamp; params) const = 0;
    virtual void Handle(const ParameterBaseamp; params) = 0;
};

class Handler1 : public HandlerBase {
public:
     class Parameter : public ParameterBase {
     public:
          Parameter(const std::stringamp; value) : ParameterBase(value) { }
          ~Parameter() { }
     };

     bool CanHandle(const ParameterBaseamp; params) const { return typeid(Parameter) == typeid(params); }
     void Handle(const ParameterBaseamp; params) {
          assert(CanHandle(params));
          const Parameteramp; p = static_cast<const Parameteramp;>(params);
          // implement callback1
     }
};

void foo(const std::vector<HandlerBase*>amp; handlers) {
     Handler1::Parameter params("value");
     for(auto handler : handlers)
         if(handler->CanHandle(params)) {
             handler->Handle(params);
             // no break: all handlers may handle params
             // with break: only first handler (if any) handles params
         }
}
 

Комментарии:

1. Как бы вы их назвали? Шаблон посетителя, например.

Ответ №9:

Я попытался использовать указатель на функцию и преобразовать std::function<int(int)>* в void*, он может быть успешно скомпилирован, но иногда это приводит к ошибке сегментации:

 int Fun(int a)
{
    std::cout << a << std::endl;
    return   (  a);
}

int main()
{
    std::function<int(int)> originPFun = Fun;
    void *ppFun;
    // ppFun = (void*)amp;Fun; // This way will cause segmentation fault
    ppFun = (void*)amp;originPFun; // This way can run seuccessful and get right result
    std::function<int(int)> *pFun = (std::function<int(int)>*)(ppFun);
    std::cout << (*pFun)(5) << std::endl;
    system("pause");
    return 0;
}