如何实现零开销的控制反转

How to implement zero-overhead Inversion of Control

几乎每个 OOP 程序员都接触过控制反转的概念。在 C++ 中,我们可以使用动态回调(即仿函数,如 lambda 和函数指针)来实现该原则。但是如果我们在编译时知道我们要将什么过程注入驱动程序,理论上我相信有一种方法可以通过将回调和 driver/signal/what-so-ever 函数组合成一个“展开的”来消除函数传递和调用的开销程序”。这是一个例子。

对于 GUI 程序,我们有关于 window 1) 设置、2) 循环和 3) 终止的逻辑。我们可以在 1) window 设置之后,2) 在每个渲染循环中,3) 和终止之前注入代码。程序化的做法是这样写:

// Snippet 1:
init_window();
init_input_handler();
init_canvas();
init_socket();
while (!window_should_close()) {
  update_window();
  handle_input();
  draw_on_canvas();
  send_through_socket();
}
drop_input_handler();
drop_canvas();
drop_socket();
terminate_window();

OOP 程序员以解耦和适当的抽象为荣。相反,我们这样写:

// Snippet 2:
init_window();
on_window_init_signal.send();
while (!window_should_close()) {
  update_window();
  on_render_signal.send();
}
on_exit_signal.send();
terminate_window();

但是如上所述,这会带来不必要的开销。我的问题是:我们如何利用 C++ 元编程机制来实现 零开销控制反转 以便代码以类似 片段 2 的形式出现可以静态地(即在编译时)转换为 snippet 1?

编辑:我能想到优化器中广泛存在的循环优化。也许这是该问题的一般化版本。

“零开销”&“但是如果我们在编译时知道要将什么过程注入到驱动程序中,”是可能的。

您可以使用模板 class 传递要调用的函数:

struct SomeInjects
{
    static void AtInit() { std::cout << "AtInit from SomeInjects" << std::endl; }
    static void AtHandleInput() { std::cout << "AtHandleInput from SomeInjects" << std::endl; }
    static void AtDraw() { std::cout << "AtDraw from SomeInjects" << std::endl; }
};

struct OtherInject
{
    static void AtInit() { std::cout << "AtInit from OtherInject" << std::endl; }
    static void AtHandleInput() { std::cout << "AtHandleInput from OtherInject" << std::endl; }
    static void AtDraw() { std::cout << "AtDraw from OtherInject" << std::endl; }
};

template < typename Mixin >
struct Win
{
    void Init()
    {    
        Mixin::AtInit();
    }    

    void HandleInput()
    {    
        Mixin::AtHandleInput();
    }    

    void Draw()
    {    
        Mixin::AtDraw();
    }    
};

int main()
{
    Win<SomeInjects> wsi; 
    wsi.Init();
    wsi.HandleInput();
    wsi.Draw();

    Win<OtherInject> wso;
    wso.Init();
    wso.HandleInput();
    wso.Draw();
}

但这有缺点,它需要静态函数。

更详细的尝试:

struct SomeInjects
{
    void AtInit() { std::cout << "AtInit from SomeInjects" << std::endl; }
    void AtHandleInput() { std::cout << "AtHandleInput from SomeInjects" << std::endl; }
    void AtDraw() { std::cout << "AtDraw from SomeInjects" << std::endl; }
};

struct OtherInject
{
    void AtInit() { std::cout << "AtInit from OtherInject" << std::endl; }
    void AtHandleInput() { std::cout << "AtHandleInput from OtherInject" << std::endl; }
    void AtDraw() { std::cout << "AtDraw from OtherInject" << std::endl; }
};

template < typename Mixin >
struct Win: Mixin
{
    void Init()
    {    
        this->AtInit();
    }    

    void HandleInput()
    {    
        this->AtHandleInput();
    }    

    void Draw()
    {    
        this->AtDraw();
    }    
};

int main()
{
    Win<SomeInjects> wsi; 
    wsi.Init();
    wsi.HandleInput();
    wsi.Draw();

    Win<OtherInject> wso; 
    wso.Init();
    wso.HandleInput();
    wso.Draw();
}

最后一种技术称为 Mixin。

如果您的编译器内联所有内容,那么一切都取决于很多事情。但是如果调用的函数不是很大,通常所有调用都是内联的。

但是如果您需要任何运行时可更改的回调,则必须使用某种可调用表示。这可以是函数指针或类似 std::function 的东西。最后一个或多或少总是产生一些小的开销。

但请记住:一个简单的取消引用指针通常根本不是速度问题。更重要的是,在这种情况下,无法传播常量,无法内联代码,因此不再可能进行整体优化。但如果需要运行时的灵活性,就会有一些成本。一如既往:优化前测量!