【问题标题】:How to store functional objects with different signatures in a container?如何在容器中存储具有不同签名的功能对象?
【发布时间】:2011-11-29 01:08:55
【问题描述】:

假设我们有 2 个函数 (void : ( void ) )(std::string : (int, std::string)),我们还可以有 10 个函数。所有(或其中一些)都接受不同的参数类型并且可以返回不同的类型。我们想将它们存储在std::map 中,所以我们得到一个这样的 API:

//Having a functions like:
int hello_world(std::string name, const int & number )
{
    name += "!";
    std::cout << "Hello, " << name << std::endl;
    return number;
}
//and
void i_do_shadowed_stuff()
{
    return;
}

//We want to be capable to create a map (or some type with similar API) that would hold our functional objects. like so:
myMap.insert(std::pair<std::string, fun_object>("my_method_hello", hello_world) )
myMap.insert(std::pair<std::string, fun_object>("my_void_method", i_do_shadowed_stuff) )
//And we could call tham with params if needed:
int a = myMap["my_method_hello"]("Tim", 25);
myMap["my_void_method"];

我想知道如何将许多不同的功能放入同一个容器中。具体来说,如何在 C++03 中使用 Boost 做到这一点。

API 应该独立于实际的函数类型(具有int a = myMap["my_method_hello"]("Tim", 25); 而不是int a = myMap&lt;int, (std::string, int)&gt;["my_method_hello"]("Tim", 25);)。

【问题讨论】:

  • 你怎么知道第一个原型调用哪些元素,第二个原型调用哪些元素?
  • 我们假设程序员知道他创建了什么 API。但是首先如何创建这样的东西呢?
  • 那为什么不使用函数(-object)指针结构呢?使用地图,您要求对仅在运行时拥有的知识提供编译时支持。这在 C++ 中没有意义。
  • 我刚刚在 stackoverflow 中读到了这个:stackoverflow.com/a/3176186/1058916,你可能会感兴趣。
  • @fefe:这让我的回答更加清晰。一点。谢谢!

标签: c++ boost


【解决方案1】:
#include <functional>
#include <iostream>
#include <string>
#include <map>

class api {
    // maps containing the different function pointers
    typedef void(*voidfuncptr)();
    typedef int(*stringcrintptr)(std::string, const int&);

    std::map<std::string, voidfuncptr> voida;
    std::map<std::string, stringcrintptr> stringcrint;
public:
    // api temp class
    // given an api and a name, it converts to a function pointer
    // depending on parameters used
    class apitemp {
        const std::string n;
        const api* p;
    public:
        apitemp(const std::string& name, const api* parent)
            : n(name), p(parent) {}
        operator voidfuncptr()
        { return p->voida.find(n)->second; }
        operator stringcrintptr()
        { return p->stringcrint.find(n)->second; }
    };

    // insertion of new functions into appropriate maps
    void insert(const std::string& name, voidfuncptr ptr)
    { voida[name]=ptr; }
    void insert(const std::string& name, stringcrintptr ptr)
    { stringcrint[name]=ptr; }
    // operator[] for the name gets halfway to the right function
    apitemp operator[](std::string n) const
    { return apitemp(n, this); }
};

用法:

api myMap; 

int hello_world(std::string name, const int & number )
{
    name += "!";
    std::cout << "Hello, " << name << std::endl;
    return number;
}

int main()
{
    myMap.insert("my_method_hello", &hello_world );
    int a = myMap["my_method_hello"]("Tim", 25);
}

不是很漂亮。更好的建议是不要做任何你想做的事情,哪怕是遥不可及的事情。

请注意,这要求所有具有相同参数的函数都返回相同的类型。

【讨论】:

  • 确实如此。我什至不会试图弄清楚这是在做什么。
  • @OliCharlesworth:已评论。但是,是的。即使你知道它在做什么,它也是一个怪物。
  • 问题是不是......所以说live......我的意思是当它的类隐藏在API深处时你可以忍受这样的东西......但是这个实现应该每次都改变引入新函数类型的时间,所以你不能只说class.push&lt;any_return_T, array_of_input_T's&gt;(function_name)
  • 正确。这仅适用于预先指定的函数类型。对于那些指定的,你可以简单地插入它们。
  • 您可以使用std::function 解决我认为的返回值问题,但我不确定如何。不过,您仍然需要提供所有可能的参数类型组合。
【解决方案2】:

你可以使用 boost::any...

#include <boost/any.hpp>
#include <iostream>
#include <map>
#include <string>

void voidFunc()
{
    std::cout << "void called" << std::endl;
}

void stringFunc(std::string str)
{
    std::cout << str << std::endl;
}

int main()
{
    std::map<std::string, boost::any> funcs;
    funcs.insert(std::pair<std::string, boost::any>("voidFunc", &voidFunc));
    funcs.insert(std::pair<std::string, boost::any>("stringFunc", &stringFunc));

    boost::any_cast<void(*)(void)>(funcs["voidFunc"])();
    boost::any_cast<void(*)(std::string)>(funcs["stringFunc"])("hello");
    return 0;
}

请注意,如果您没有在 any_cast 中正确指定函数签名,则会出现运行时异常。

【讨论】:

  • 我们能否在插入地图时通过 Boost.Variant 自动(在编译时)类型验证?
  • @myWallJSON:不确定我是否理解正确,但可能是的。如果将boost::any 替换为boost::variant&lt;T1, T2, T3&gt;,则只能将这三种类型插入到地图中。
  • @MSalters:见专用Q
【解决方案3】:

完整示例herehere

我们可以像 boost::any 这样使用类型擦除。但是由于我们只想保存函数,所以我们可以想出更方便的用法。

我们首先需要的是函数签名推导。见here

template <typename T>
struct function_traits : public function_traits<decltype(&T::operator())>
{};

template<typename R, typename ...Args> 
struct function_traits<std::function<R(Args...)>>
{
    typedef R result_type;
    typedef typename std::function<R(Args...)> type;
    typedef typename std::function<void(Args...)> typeNoRet;
};

template<typename R, typename ...Args> 
struct function_traits<R(*)(Args...)>
{
    typedef R result_type;
    typedef typename std::function<R(Args...)> type;
    typedef typename std::function<void(Args...)> typeNoRet;
};

template<typename R, typename cls, typename ...Args> 
struct function_traits<R(cls::*)(Args...)>
{
    typedef R result_type;
    typedef typename std::function<R(Args...)> type;
    typedef typename std::function<void(Args...)> typeNoRet;
};

要实现类型擦除,我们需要不是模板的基类和派生的模板类。 FunctionHolder 将存储基类指针。构造函数将使用 function_traits 来确定正确的派生类类型。

class FunctionHolder {
    private:
        struct BaseHolder {
            BaseHolder() {}
            virtual ~BaseHolder() {}
        };

        template <typename T>
        struct Holder : public BaseHolder {
            Holder(T arg) : mFptr(arg) {}

            template<typename... Args>
            void Call(Args&&...args) {
                mFptr(std::forward<Args>(args)...);
            }

            template<typename R, typename... Args>
            R CallRet(Args&&...args) {
                return mFptr(std::forward<Args>(args)...);
            }

            T mFptr;
        };

    public:
        template<typename T>
        FunctionHolder(T t) : mBaseHolder(new Holder<typename function_traits<T>::type>(t))
                            , mBaseHolderNoRet(new Holder<typename function_traits<T>::typeNoRet>(t)) {}

        template<typename T, typename...Args>
        FunctionHolder(T&& t, Args&&... args) : mBaseHolder(new Holder<typename function_traits<T>::type>
                                                    (std::bind(std::forward<T>(t), std::forward<Args>(args)...)))
                                              , mBaseHolderNoRet(new Holder<typename function_traits<T>::typeNoRet>
                                                    (std::bind(std::forward<T>(t), std::forward<Args>(args)...))) {}

    void operator()() {
        this->operator()<>();
    }

    template<typename... Args>
    void operator()(Args&&... args) {
        auto f = dynamic_cast<Holder<std::function < void(Args...) > >*>(mBaseHolderNoRet.get());
        if (f) {
            f->Call(std::forward<Args>(args)...);
            return;
        }
        throw std::invalid_argument("");
    }

    template<typename R, typename... Args>
    R call(Args&&... args) {
        auto f = dynamic_cast<Holder<std::function<R(Args...)>>*>(mBaseHolder.get());
        if (f) {
            return f->template CallRet<R>(std::forward<Args>(args)...);
        }
        throw std::invalid_argument("");
    }

    private:
        std::unique_ptr<BaseHolder> mBaseHolder;
        std::unique_ptr<BaseHolder> mBaseHolderNoRet;
};

在这种情况下,FunctionHolder 存储了 BaseHolder 的 2 个指针,第一个具有正确的签名,第二个返回 void。为避免开销,如果您要始终指定返回值或从不使用它,您可以删除其中一个。

最后我们可以这样使用它。

struct st0 {
    st0(int x) : mX(x) {}

    std::string  print(int p) {
        std::cout << "st0::print " 
                  << mX << " " << p << std::endl;
        return "ret_from_st0::print";
    }
    int mX;
};

struct st1 {
    st1(int x) : mX(x) {}

    void operator()() {
        std::cout << "st1::operator() " 
                  << mX << " " << std::endl;
    }
    int mX;
};

void Func0(int a, int b) {
    std::cout << "Func0. "
              << " a: " << a
              << " b: " << b << std::endl;
}

void Func1(int a, int b, std::string str) {
    std::cout << "Func0. "
              << " a: " << a
              << " b: " << b
              << " str: " << str << std::endl;
}

uint64_t Func2(int a, int b, std::string str) {
    std::cout << "Func0. "
              << " a: " << a
              << " b: " << b
              << " str: " << str << std::endl;
    return 0xBAB0CAFE;
}

int main() {
    try {
        // void(int, int)
        FunctionHolder ex1(&Func0);
        ex1(1,12);
    
        // void(int, int, std::string)
        FunctionHolder ex2(&Func1);
        ex2(1, 12, std::string("Some text here"));
        
        // int(int, int, std::string)
        // call and print return value
        FunctionHolder ex3(&Func2);
        std::cout << "Ret: " << std::hex << ex3.call<uint64_t>(123, 3211, std::string("another text")) 
                  << std::dec << std::endl;
        // call and drop return value
        ex3(123, 3211, std::string("another text"));
    
        // Hold std::function<void(int)>
        std::function<void(int)> ex4 = std::bind(&Func0, 1, std::placeholders::_1);
        FunctionHolder c(std::function<void(int)>(std::bind(&Func0, 1, std::placeholders::_1)));
        ex4(12);
    
        // will bind to st0 member function print
        st0 st0object(8955);
        FunctionHolder ex5(&st0::print, st0object, std::placeholders::_1);
        ex5(2222);
        // call and print return value
        std::cout << "Ret: " << ex5.call<std::string>(7531) << std::endl;
    
        // wrap lambda function with std::function and pass to holder
        FunctionHolder ex6(std::function<void(void)>([]() {std::cout << "lambda function called" << std::endl;}));
        ex6();
    
        // functor object st1
        FunctionHolder ex7(st1(123654));
        ex7();
        
        // Will throw, because st1::operator() gets no arguments
        ex7(123);
    } catch (std::invalid_argument &e) {
        std::cout << "Invalid argument(s) were passed" << std::endl;
        // error handling here...
    }

    return 0;
}

FunctionHolder 可以存储类似 C 的函数指针、函子对象、lambda 函数和成员函数指针。 只有 2 个例外需要记住。

  1. 如果您打算使用 std::bind 将对象指针绑定到成员函数,您可以将所有参数传递给 FunctionHolder 构造函数,该构造函数将为您进行绑定。如果要绑定任何其他函数参数,则必须手动绑定它,构造 std::function 并将其传递给 FunctionHolder。
FunctionHolder fh(std::function<void(std::string)>(std::bind(&Func1, 888, 333, std::placeholders::_1)));
fh(std::string("Ok."));
  1. 要存储 lambda 函数,首先用 std::function 包装它们,如上例所示。

在传递错误参数的情况下,FunctionHolder 将抛出 std::invalid_argument。

【讨论】:

    【解决方案4】:

    问题是,不知何故,当您调用函数时,您已经知道它们将是什么类型。

    如果我们做类似的事情

    int x = map["key"](1, "2")
    

    我们已经可以推断出存储在“key”中的任何函数都是(int (*)(int, char*))类型的,所以我们不妨做类似的事情

    int x = map_of_int_and_string_to_int["key"](1, "2");
    

    并避免将所有键合并在一起的所有麻烦......虽然 C++ 确实有一些重载功能正是针对这类东西,但我真的不明白为什么你应该在这种特殊情况下打扰。

    最后,您为什么要将所有这些功能放在同一个地图中?它们不共享任何类似的接口,因此您不能统一访问它们,不能迭代它们,也不能不透明地将它们传递给其他人。如果没有任何共同点,您就无法安全地对这张假设地图中的功能做任何事情。

    【讨论】:

    • 我尝试通过 MPI 实现一个任务池。所以我需要某种 RPC,但它可以在我的程序的不同部分之间工作,这意味着处理器 A 希望处理器 B 使用参数 D 调用函数 C。我们不能像处理线程那样在进程之间传递指向函数的指针,所以我们需要一些包装容器来保存每个流程实例的函数指针。全部在一个源文件\一个程序中......
    • @myWallJSON:你打算如何传递抽象参数?
    【解决方案5】:

    您可以通过将函数指针转换为 void 指针并返回来实现。您应该在运行时知道签名,因此硬连线铸造操作员不会成为问题。然而,这样做的逻辑让我无法理解。这根本没有意义,至少在 C++ 中是这样。使用模板类/函数或函数指针结构更有意义。

    例如,使用模板:

    template <typename X> foo(X param1) { /* do something with param1*/};
    template <typename X, typename Y> foo(X param1, Y param2)
       {/* do something with 2 params*/};
    template <int X> foo(X param1) { /* only one parameter, which is int */};
    

    现在:

    foo(5); // calls the third one
    foo("5"); // calls the first one
    foo("5", 5); // calls the second one.
    

    谁需要地图?

    【讨论】:

    • 这里为什么需要模板?
    • @Oli - 只是一个例子,不必使用模板,您可以通过各种不同的方式实现它。在我的特定示例中,我试图展示一个接受任何类型参数(通过模板)的通用函数,以及一个接受 int 的特定函数,但它可能看起来有点强迫。
    • 你在这里所做的只是超载。您如何将这些功能放在一个容器中?这就是问题所在。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-30
    • 1970-01-01
    • 1970-01-01
    • 2021-10-26
    相关资源
    最近更新 更多