如何在C++中通过函数名(std::string)调用函数?

39

我想知道是否有一种简单的方法可以通过一个字符串来调用一个函数。我知道一种简单的方法,可以使用“if”和“else”语句。

int function_1(int i, int j) {
    return i*j;
}

int function_2(int i, int j) {
    return i/j;
}

...
...
...

int function_N(int i, int j) {
    return i+j;
}

int main(int argc, char* argv[]) {
    int i = 4, j = 2;
    string function = "function_2";
    cout << callFunction(i, j, function) << endl;
    return 0;
}

这是基本方法

int callFunction(int i, int j, string function) {
    if(function == "function_1") {
        return function_1(i, j);
    } else if(function == "function_2") {
        return function_2(i, j);
    } else if(...) {

    } ...
    ...
    ...
    ...
    return  function_1(i, j);
}

有更简单的东西吗?

/* New Approach */
int callFunction(int i, int j, string function) {
    /* I need something simple */
    return function(i, j);
}
5个回答

68

你所描述的被称为反射,C++不支持它。然而,你可以想出一些解决办法,比如在这个具体的情况下,你可以使用一个std::map将函数名(std::string对象)映射到函数指针,在函数原型完全相同的情况下,这可能比看起来更容易:

#include <iostream>
#include <map>

int add(int i, int j) { return i+j; }
int sub(int i, int j) { return i-j; }

typedef int (*FnPtr)(int, int);

int main() {
    // initialization:
    std::map<std::string, FnPtr> myMap;
    myMap["add"] = add;
    myMap["sub"] = sub;

    // usage:
    std::string s("add");
    int res = myMap[s](2,3);
    std::cout << res;
}

请注意,myMap[s](2,3)会取出与字符串s映射的函数指针并调用该函数,向其传递参数23,从而使得此示例的输出为5


1
你可以使用std::function和初始化列表语法稍微改进一下。 - Martin York
@LokiAstari: 我猜std::function是C++11的。 - LihO
1
@AlanValejo:有可能您没有可用的C++11支持。尝试使用#include <functional>,如果不起作用,请尝试使用#include <tr1/functional> - LihO
1
@AlanValejo:tr1代表技术报告,即使C++11尚未被支持,它也可能允许您使用一些C++11功能。但是可能不会提供TR1。 - LihO
请查看此帖子:http://stackoverflow.com/questions/19480281/how-to-call-a-function-by-its-name-an-approach-using-stlmap-and-class - Alan Valejo
显示剩余5条评论

30

利用标准字符串到标准函数的映射。

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

int add(int x, int y) {return x+y;}
int sub(int x, int y) {return x-y;}

int main()
{
    std::map<std::string, std::function<int(int,int)>>  funcMap =
         {{ "add", add},
          { "sub", sub}
         };

    std::cout << funcMap["add"](2,3) << "\n";
    std::cout << funcMap["sub"](5,2) << "\n";
}

使用 Lambda 更好:

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

int main()
{
    std::map<std::string, std::function<int(int,int)>>  funcMap =
         {{ "add", [](int x, int y){return x+y;}},
          { "sub", [](int x, int y){return x-y;}}
         };

    std::cout << funcMap["add"](2,3) << "\n";
    std::cout << funcMap["sub"](5,2) << "\n";
}

@AlanValejo:当然可以 :) 在C++11中,许多东西变得更加清晰。 - LihO
可以将其与字符串或boost::any一起使用吗? - Lixt
@Lixt 它已经在使用std::string,不确定您为什么要在这种情况下使用boost::any - Martin York

7
你也可以将你的函数放到一个共享库中。你可以使用dlopen()动态加载这样的库,然后使用std::string调用函数。这里是一个例子:
hello.cpp
#include <iostream>

extern "C" void hello() {
    std::cout << "hello" << '\n';
}

main.cpp

#include <iostream>
#include <dlfcn.h>

int main() {
    using std::cout;
    using std::cerr;

    cout << "C++ dlopen demo\n\n";

    // open the library
    cout << "Opening hello.so...\n";
    void* handle = dlopen("./hello.so", RTLD_LAZY);

    if (!handle) {
        cerr << "Cannot open library: " << dlerror() << '\n';
        return 1;
    }

    // load the symbol
    cout << "Loading symbol hello...\n";
    typedef void (*hello_t)();

    // reset errors
    dlerror();

    std::string yourfunc("hello"); // Here is your function

    hello_t hello = (hello_t) dlsym(handle, yourfunc.c_str());
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        cerr << "Cannot load symbol 'hello': " << dlsym_error <<
            '\n';
        dlclose(handle);
        return 1;
    }

    // use it to do the calculation
    cout << "Calling hello...\n";
    hello();

    // close the library
    cout << "Closing library...\n";
    dlclose(handle);
}

编译:

g++ -fPIC -shared hello.cpp -o hello.so

并且:

g++ main.cpp -o main -ldl

运行:

C++ dlopen demo

Opening hello.so...
Loading symbol hello...
Calling hello...
hello
Closing library...

这个例子是从这里偷来的。你可以在这里找到更详细关于dlopen()和c++的解释。


我怀疑这就是虚幻引擎的做法;它会创建一个游戏的.dll文件,其中可能包含游戏特定的代码,然后可以引用该代码。例如,在输入处理中,您可以通过名称将函数绑定为字符串以执行输入操作。 - Casey

4

还有一种可能性尚未提及,即真正的反射。

一种选项是使用操作系统功能来解析名称以获取地址,从可执行文件或共享库中访问导出的函数。这具有有趣的用途,例如将两个“竞争者”dll加载到“裁判”程序中,使人们可以通过让他们的实际代码相互对抗(玩Reversi或Quake等)进行搏斗。

另一个选项是访问编译器创建的调试信息。在Windows下,对于兼容的编译器,这可能非常容易,因为所有工作可以转移到系统dll或可从Microsoft下载的免费dll中。部分功能已包含在Windows API中。

但是,这更属于系统编程类别 - 无论使用什么语言 - 因此它只适用于C ++,因为它是卓越的系统编程语言。


这似乎几乎是我上面回答中缺失的一部分,也正是我的答案所做的事情,只不过方式不同:“使用操作系统函数来将名称解析为地址”,而我的方法则更加直接,需要您自己编写算法将字符串解码为地址。 - AMDG
1
反射的概念是访问编译器创建的元数据,而不是必须定义包含函数名称和指针的表格。除了导出的名称和调试符号外,还有一些编译器支持的RTTI,以及混合方法,例如处理编译器创建的.map文件以在函数名称和地址之间进行映射。JEDI项目用于堆栈跟踪,但也可用于查找名称的地址。该想法是使用反映源代码的编译器生成数据,而不是必须定义自己的表格。 - DarthGizka

1

与jav的答案类似,但从当前程序加载函数:

#include <iostream>
#include <dlfcn.h>

extern "C"
void test()
{
    std::cout << __PRETTY_FUNCTION__ << "\n";
}

extern "C"
void test2()
{
    std::cout << __func__ << "\n";
}

void something_the_user_is_not_supposed_to_call()
{
    std::cout << "Oops...\n";
}

int main(int argc, char** argv)
{
    if (argc < 2)
    {
        std::cout << "Usage: " << argv[0] << " [function_name]\n";
        return 1;
    }

    void* self = dlopen(nullptr, RTLD_LAZY);

    if (!self)
    {
        std::cerr << dlerror() << "\n";
        return 1;
    }

    void* function = dlsym(self, argv[1]);
    
    if (!function)
    {
        std::cerr << dlerror() << "\n";
        return 1;
    }

    reinterpret_cast<void(*)()>(function)();

    dlclose(self);
}

然后使用-rdynamic选项进行编译,以便导出所有符号:

$ g++ test.cpp -o test -ldl -rdynamic

它有效:

$ ./test test
void test()
$ ./test test2
test2
$ ./test test3
./test: undefined symbol: test3

当然,这样做是一个可怕的想法:

$ ./test _Z42something_the_user_is_not_supposed_to_callv
Oops...
$ ./test abort
Aborted (core dumped)
$ ./test sleep
^C
$ ./test main
Segmentation fault (core dumped)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接