【问题标题】:Store functions with different signatures in a map在地图中存储具有不同签名的函数
【发布时间】:2017-08-16 13:40:35
【问题描述】:

我正在尝试在 C++ 中创建一个以 string 为键的 map 和一个作为 value 的通用方法,但我不知道这是否可能。我想做这样的事情:

void foo(int x, int y)
{
   //do something
}

void bar(std::string x, int y, int z)
{
   //do something
} 

void main()
{
   std::map<std::string, "Any Method"> map;

   map["foo"] = &foo;      //store the methods in the map
   map["bar"] = &bar;

   map["foo"](1, 2);       //call them with parameters I get at runtime
   map["bar"]("Hello", 1, 2);
}

这可能吗?如果是,我怎么能意识到这一点?

【问题讨论】:

  • 然后您可以阅读文档。 &amp;foo&amp;bar 是不同的类型。第一种是(void *)(int, int),第二种是(void *)(std::string, int, int)。所以你需要有不同的想法。
  • 顺便说一句,你最初的任务是什么?
  • 当您尝试致电map["foo"]("Hello") 时会发生什么?
  • 也许你应该看看here
  • @suraznegi 这很不安全...

标签: c++


【解决方案1】:

您可以将函数类型键入擦除到容器中,然后提供模板operator()。如果你弄错了,这将抛出std::bad_any_cast

注意由于类型擦除,您必须在调用站点指定完全匹配的参数,例如std::function&lt;void(std::string)&gt;std::function&lt;void(const char *)&gt; 不同,尽管两者都可以使用 "Hello" 之类的值调用。

#include <any>
#include <functional>
#include <map>
#include <string>
#include <iostream>

template<typename Ret>
struct AnyCallable
{
    AnyCallable() {}
    template<typename F>
    AnyCallable(F&& fun) : AnyCallable(std::function(std::forward<F>(fun))) {}
    template<typename ... Args>
    AnyCallable(std::function<Ret(Args...)> fun) : m_any(fun) {}
    template<typename ... Args>
    Ret operator()(Args&& ... args) 
    { 
        return std::invoke(std::any_cast<std::function<Ret(Args...)>>(m_any), std::forward<Args>(args)...); 
    }
    std::any m_any;
};

void foo(int x, int y)
{
    std::cout << "foo" << x << y << std::endl;
}

void bar(std::string x, int y, int z)
{
    std::cout << "bar" << x << y << z << std::endl;
} 

using namespace std::literals;

int main()
{
    std::map<std::string, AnyCallable<void>> map;
    
    map["foo"] = &foo;      //store the methods in the map
    map["bar"] = &bar;
    
    map["foo"](1, 2);       //call them with parameters I get at runtime
    map["bar"]("Hello, std::string literal"s, 1, 2);
    try {
        map["bar"]("Hello, const char *literal", 1, 2); // bad_any_cast
    } catch (std::bad_any_cast&) {
        std::cout << "mismatched argument types" << std::endl;
    }
    map["bar"].operator()<std::string, int, int>("Hello, const char *literal", 1, 2); // explicit template parameters
    
    return 0;
}

【讨论】:

  • @buttonsrtoys iirc VS2017不完全兼容,可能缺少std::function的推演指南
  • map["foo"](1, 2) 更改为int n = 1; map["foo"](n, 2); 在运行时失败并显示bad_any_cast。呼叫转换为std::function&lt;void(int&amp;,int&amp;)&gt;,而any 具有std::function&lt;void(int,int)&gt;。这使得地图使用起来非常棘手——有什么想法可以缓解这个问题吗? (我在 gcc-mp-8 (MacPorts gcc8 8.2.0_0) 8.2.0 上使用 c++2a 模式——不确定这是否是一个因素)。
  • @RandomBits 不,这种技术非常脆弱。您可以通过显式命名声明类型来缓解它,例如map["foo"].operator()&lt;int, int&gt;(n, m);,但这很丑,而且很容易出错
  • @Caleth 这给了我AnyCallable(std::function(fun)) {} 所在行的错误。它说“std::function”:使用类模板需要模板参数列表。
  • @Caleth:如何生成返回值而不是将其硬编码为void?此外,当使用变量时,只写int i = 1; map["foo"].operator()&lt;int, int&gt;((int) i, 2); 似乎也有效。最后一次强制转换是必要的,否则将无法编译。这个解决方案仍然非常不安全,并且不像“正常”函数调用那样。
【解决方案2】:

您能做的最多(我不能在这里说最好)就是使用签名擦除。这意味着将指向函数的指针转换为通用签名类型,然后在使用它们之前将它们转换回正确的签名。

这只能在非常特殊的用例(我无法想象现实世界的用例)中完成,并且非常不安全:没有什么能阻止您将错误的参数传递给函数。简而言之:永远不要在真实世界代码中这样做

话虽如此,下面是一个工作示例:

#include <iostream>
#include <string>
#include <map>

typedef void (*voidfunc)();

void foo(int x, int y)
{
    std::cout << "foo " << x << " " << y << std::endl;
}

void bar(std::string x, int y, int z)
{
    std::cout << "bar " << x << " " << y << " " << z << std::endl;
}

int main()
{
    std::map<std::string, voidfunc> m;
    m["foo"] = (voidfunc) &foo;
    m["bar"] = (voidfunc)& bar;
    ((void(*)(int, int)) m["foo"])(1, 2);
    ((void(*)(std::string, int, int)) m["bar"])("baz", 1, 2);
    return 0;
}

它按预期给出:

foo 1 2
bar baz 1 2

我无法在标准中找到这是否调用未定义的行为,因为关于函数指针转换的说法很少,但我很确定所有常见的编译器都接受这一点,因为它只涉及 函数指针铸造。

【讨论】:

  • 你总是可以制作一个 std::any of std::functions ... 我不会惊讶这有同样的潜在 UB 问题,但至少通过使用给出了定义的外观标准库结构
【解决方案3】:

您不能将具有不同签名的函数存储在像map 这样的容器中,无论您将它们存储为函数指针还是std ::function&lt;WHATEVER&gt;。在这两种情况下,关于函数签名的信息都是一个且只有一个。

map 中的value 的类型为一,表示其中存储的对象都是同一类型

因此,如果您的函数具有所有相同的签名,那么这很容易,否则,您必须放弃类型安全并开始走在一个非常危险的领域。 您可以在其中擦除关于存储在地图中的函数的类型信息。 这转化为类似map&lt;string, void*&gt;

【讨论】:

  • 将函数指针保存为void* 时,您必须先将其转换回实际签名,然后才能调用它,至少在我的测试中是这样!?所以我认为使用映射来存储具有不同签名的函数指针没有多大意义。更好地使用具有虚函数或重载方法的类。
  • @xander 是的,您必须将其重新转换才能使用它。使用类允许获得相同的结果,但您并没有真正存储函数。这就是为什么它不包含在我的答案中。不过好点。
猜你喜欢
  • 1970-01-01
  • 2011-11-11
  • 1970-01-01
  • 2011-05-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-07
相关资源
最近更新 更多