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

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

e.g.

void func1(const std::string& value);

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

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

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

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


person ksl    schedule 02.10.2014    source источник
comment
Почему бы просто не дать им одинаковые подписи. Вам не обязательно использовать все параметры в функции   -  person Ed Heal    schedule 02.10.2014
comment
Почему вам нужно использовать вектор для этого? Вектор может содержать только те типы, которые имеют один и тот же тип (учитывается полиморфизм). Если у вас есть функции с разными сигнатурами, вы не сможете сделать, например. for( auto&& function : functions){ function(); }, поэтому вам все равно нужно держать их разделенными или унифицировать их подписи (но если у них разные подписи, этот последний вариант выглядит как грубое принуждение их к вектору...)   -  person JBL    schedule 02.10.2014
comment
@EdHeal - Думаю, я мог бы. Просто интересовался вариантами.   -  person ksl    schedule 02.10.2014
comment
@JBL - В конце концов, это то, что я хотел сделать. Я предполагаю, что вы говорите, что это невозможно.   -  person ksl    schedule 02.10.2014
comment
Что вы точно хотите сделать? Посмотрите на эти разные ответы. Я проголосую за закрытие, если непонятно, о чем спрашивать.   -  person ikh    schedule 02.10.2014
comment
Вы сформулировали свой вопрос с точки зрения возможных решений. Что вам нужно сделать, так это указать, чего вы на самом деле хотите достичь. Я думаю, что еще есть время, чтобы сделать это до того, как сложатся голоса за закрытие.   -  person quamrana    schedule 02.10.2014
comment
В качестве альтернативы, как вы собираетесь использовать этот вектор функций? Это то, что нам нужно знать. Нет смысла спрашивать, как впихнуть эти объекты в вектор. Помещение материала в вектор никогда не бывает интересным само по себе. Использование вектора позже, возможно, чтение из них, интересно   -  person Aaron McDaid    schedule 10.08.2015


Ответы (9)


Вы не сказали, что вы ожидаете сделать с 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::string& 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,
    [&](const std::string& s){ func2(s, func2_arg1, func2_arg2); }
};

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

person Jonathan Wakely    schedule 02.10.2014
comment
Спасибо за ваш ответ. Использование std::bind так, как вы показали (поскольку я знаю аргументы во время компиляции), - это то, что я пробовал, когда сказал, что экспериментировал с std::bind, но не смог заставить его работать. Теперь я могу благодаря вашему ответу. - person ksl; 02.10.2014

То, что вы хотите, возможно через 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 ( auto&& myfun : v ) {
        (*myfun)();
    }
    return 0;
}

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

person Svalorzen    schedule 02.10.2014

Для C++ 17 std::variant можно использовать для хранения std::function с разными подписями. В этом случае функция 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(auto& 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");
  }
}
person alex_noname    schedule 30.12.2020

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

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

receive_message(buffer& buf)
  switch(msg_type(buf))
    case A: 
    case B:
    ...

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

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

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

vector <std::function<void(buffer& buf)>> handlers;
person bobah    schedule 02.10.2014

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

person MSalters    schedule 02.10.2014
comment
это не вопрос - person BeyelerStudios; 02.10.2014
comment
@BeyelerStudios: он явно просит альтернативу vector<> для выполнения двух функций. Разве tuple<> не способен на это? - person MSalters; 02.10.2014
comment
по ряду функций обратного вызова я понимаю, что число произвольное во время выполнения, кортежи здесь не годятся - person BeyelerStudios; 02.10.2014
comment
Даже в этом случае вполне вероятно, что набор довольно ограничен, и вы просто установите функции, которые вам не нужны, nullptr. - person MSalters; 02.10.2014

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

person pmr    schedule 02.10.2014
comment
Спасибо. Я смог получить то, что хотел, используя std::bind после ответа Джонатана Уэйкли. - person ksl; 02.10.2014

Не уверен, насколько это будет полезно для вас, он основан на boost::any, избыточные параметры игнорируются. Вы можете добавить try...catch для 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::string& value)
{
    std::cout << "func1 " << value << std::endl;
}

void func2(const std::string& 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(&func1));    
    functions.push_back(make_generic_function_ptr(&func2));

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

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

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

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

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

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

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

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

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

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

void foo(const std::vector<HandlerBase*>& 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
         }
}
person BeyelerStudios    schedule 02.10.2014
comment
Как бы вы их назвали? Шаблон посетителя, например. - person MSalters; 02.10.2014

Я попытался использовать указатель функции и преобразовал 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*)&Fun; // This way will cause segmentation fault
    ppFun = (void*)&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;
}
person 0811张庆昊    schedule 30.12.2020