首页 > 解决方案 > 在 main() 之前调用的 C++ 注册函数

问题描述

我的程序看起来像这样:

map<string, function<void(const MyType&)>> callables;

int main(int argc, char *argv[]) {
    string name = GetFromSomewhere();
    auto iter = callables.find(name);
    if (iter != callables.end()) {
        MyType my_thing = GetSomeValue();
        iter->second(my_thing);
    }
}

换句话说,我有一个函数表,main 将执行一些操作,在该表中生成一个查找键,进行查找,如果成功,则调用该函数。

现在我可以在定义映射的翻译单元中初始化表,但这意味着想要在该映射中的每个新函数都必须修改映射的 TU。这变得很麻烦。

最好有注册功能:

void RegisterCallable(const string&, function<void(MyType)>);

然后任何想要在表格中添加内容的开发人员只需调用RegisterCallable()

# In foo.cc:
void NiftyCallable(const MyType& thing) { ... }

RegisterCallable("nifty", NiftyCallable);

过去使用stringvs的经验char[]警告我,我要求痛苦,但我无法(重新)找到特定的 C++ 规则,该规则告诉我何时RegisterCallable()调用我们将分散在代码库中的那些调用(在特别是,如果保证在 main 执行之前调用它们,或者 TU 可以稍后按需加载——那么我的痛苦记忆对于 C++14 是否正确)。

我是否记错了这会引起疼痛?或者除了要求一些 TU 了解(目前大约 100 个)需要注册的功能之外,还有更好的方法吗?

标签: c++c++14

解决方案


不要把表放在全局范围内,把它放在函数范围内(仍然必须是静态的,以确保它在应用程序的长度内存在)。所以你可以强制初始化顺序。然后你解决了跨编译单元的初始化顺序问题。

static std::map<std::string, std::function<void(const MyType&)& getCallables() {
     static std::map<std::string, std::function<void(const MyType&)>> callables;
  // ^^^^^^  Static storage duration object.
  //         lives as long as the application.
     return callables;
}

int main(int argc, char *argv[]) {
    std::string name = GetFromSomewhere();
    auto iter = getCallables().find(name);
    if (iter != getCallables().end()) {
        MyType my_thing = GetSomeValue();
        iter->second(my_thing);
    }
}

当从任何范围调用以注册一个新函数时,它会调用getCallables()强制初始化。所以你避免了初始化顺序问题。

void RegisterCallable(const std::string& name, std::function<void(MyType)> f)
{
    getCallables()[name] = f;
}

不幸的是,您不能直接在 C++ 的编译单元中进行独立的函数调用(与许多解释语言不同)。

// So this will not work
RegisterCallable("nifty", NiftyCallable);

所以这样做的方法是在全局范围内声明对象,其构造函数注册了该对象。

struct DoRegisterCallable {
    DoRegisterCallable(std::string const& name, std::function<void(MyType)> f) {
       RegisterCallable(name, f);
    }
};

现在在您的编译单元中,添加该功能的人将执行以下操作:

// In foo.cc:
void NiftyCallable(const MyType& thing) { ... }   
DoRegisterCallable niftyCallableRegister("nifty", NiftyCallable);

在上面的评论中,IgorTandetnik 建议niftyCallableRegister可能不包含在可执行文件中,因为编译器可能会优化变量。这种说法不正确,但值得思考。

如果文件 foo.cc 被编译成静态库。然后这个静态库链接到可执行文件,那么就有可能不包含它。但在正常情况下,大多数构建都是使用动态库而不是静态库完成的(因为静态库有很多其他问题,人们大多已经停止使用它们),所以这在正常操作中是次要问题(但需要考虑)。

此外,如果文件范围、静态存储持续时间变量在 main 或 deferred 之前初始化,则它是实现定义的。这很容易通过一些单元测试进行测试,因为它是编译器的属性而不是未定义的行为(如果您是编译器正在执行此操作,那么您需要检查文档以查看是否可以更改行为)。

我对标准中这种语言的推测是允许延迟加载共享库直到应用程序启动后,但仍保证它们的行为符合标准。这种编写方式允许应用程序动态加载共享库并在应用程序 main() 启动后对其进行初始化(确保文件范围静态存储持续时间对象已初始化)。一个角落案例,可以通过单元测试轻松测试。


我可能会决定将它包装在一个类中以便于使用:

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

class MyType
{
};

using Callable = std::function<void(MyType)>;
using CallableMap = std::map<std::string, Callable>;

class Callables
{

    static CallableMap& getCallables()
    {
        static CallableMap  callables;
        return callables;
    }

    public:
        static void registerFunc(std::string name, std::function<void(MyType)>&& f)
        {
            getCallables()[std::move(name)] = std::move(f);
        }
        static void call(std::string const& name, std::function<MyType()>&& getter)
        {
            auto find = getCallables().find(name);
            if (find != getCallables().end()) {
                find->second(getter());
            }
        }
        static void call(std::string const& name, MyType const& value)
        {
            call(name, [&value](){return value;});
        }
};
struct RegisterCallables
{
    RegisterCallables(std::string value, Callable&& f)
    {
        Callables::registerFunc(std::move(value), std::move(f));
    }
};

void echo(MyType v)
{
    std::cout << "Echo\n";
}
RegisterCallables   echoRegister("echo", echo);

int main()
{
    MyType d;
    Callables::call("echo", d);
}

推荐阅读