4.More C++ Idioms - Attach by Initialization惯用法and more

enter image description here

Attach a user-defined object to a framework before program execution begins

C++ 习惯用法:Attach by Initialization

定义与目的

“Attach by Initialization”惯用法意在“在程序执行开始之前,将用户定义的对象附着到一个框架上”​ en.wikibooks.org 。换言之,它利用全局/静态对象在 main 之前构造的机制,将自定义对象注册或附加到某个应用框架中。该惯用法也被称为“静态对象构造器”(Static-object-with-constructor)​ en.wikibooks.org 。通过在程序启动时创建一个全局对象,其构造函数可以执行初始化逻辑,从而在框架的主事件循环启动前完成应用级的设置。

典型使用场景

该惯用法常见于GUI 框架或对象请求代理等需要内部控制主循环的系统中​ en.wikibooks.org 。例如,Microsoft MFC 等GUI框架往往将 main 隐藏在框架内部(如 AfxWinMain),开发者无法直接编写自己的 main。此时,可以在全局定义一个框架应用类的对象,在其构造过程中执行初始化。​ en.wikibooks.org 指出:“某些应用程序框架(如GUI框架(例如 MFC)和对象请求代理(例如某些 CORBA 实现))使用其自身的内部事件循环来控制整个应用程序……Attach by Initialization是一种在框架控制的循环开始执行之前执行应用程序特定代码的方法”​ en.wikibooks.org 。除了 MFC,这一思想也适用于需要插件式扩展或自动注册服务/回调的场景:通过静态对象构造函数向系统注册功能或资源。 示例:使用 MFC 时,定义一个继承自 CWinApp 的类 HelloApp,并在全局创建它的实例。该实例在程序启动时构造,从而向 MFC 框架注册应用程序。Wiki 示例中提到的 HelloApp 全局对象 myApp 会在 main 之前构造,并在其构造过程中触发框架的初始化过程​ en.wikibooks.org 。 已知使用:该惯用法的典型用例就是 Microsoft MFC 框架​ en.wikibooks.org 。在该框架下,AfxWinMain(MFC 等价于 main)会在运行时检索先前注册的全局应用对象并调用其初始化函数。

示例代码

下面以 MFC 为例说明用法(来源见 Wiki Book)​ en.wikibooks.org ​ en.wikibooks.org :

// Hello.h
class HelloApp: public CWinApp {
public:
    virtual BOOL InitInstance();
};
// Hello.cpp
#include <afxwin.h>
#include "Hello.h"
HelloApp myApp;  // 全局“应用”对象

BOOL HelloApp::InitInstance() {
    m_pMainWnd = new CFrameWnd();
    m_pMainWnd->Create(0, "Hello, World!!");
    m_pMainWnd->ShowWindow(SW_SHOW);
    return TRUE;
}

上述代码中,HelloApp myApp; 作为全局对象,在程序启动时构造。Wiki Book 解释道:“关键在于全局对象 myApp(类型为 HelloApp)在 main 执行之前就被默认初始化……CWinApp 构造函数被调用,它会触发框架中多个类的构造。在这些构造的执行过程中,全局对象被附着(attached)到框架中”​ en.wikibooks.org 。也就是说,当 MFC 的 AfxWinMain 运行时,它会发现先前初始化的 myApp,并调用其 InitInstance(),从而在框架主循环启动前完成应用程序的窗口创建。

设计思想

“Attach by Initialization”利用了 C++ 静态存储期对象的特性:全局或命名空间作用域的静态对象会在 main 开始执行前构造​ en.wikibooks.org 。如 Wiki 所述:“在 C++ 中,全局对象和命名空间中的静态对象会在 main 开始之前被初始化,它们也称为静态存储期对象”​ en.wikibooks.org 。因此,通过将初始化代码放入一个静态对象的构造函数,就可以在程序入口之前执行这段代码。这与 RAII(资源获取即初始化)思想相似:通过对象的构造器获取资源或注册服务,在析构时释放或注销。Attach-by-Initialization 可以看做是一种静态注册(self-registration)模式:全局对象自我注册到框架或系统中,省去了显式调用注册函数的步骤。同时,该惯用法体现了利用对象生命周期控制初始化和清理逻辑的理念。

警示与注意事项

使用 Attach by Initialization 时需注意 静态初始化顺序 问题。C++ 标准只保证同一编译单元内的静态对象按定义顺序初始化,但 不同编译单元之间的初始化顺序是未定义的​ en.wikibooks.org 。Wiki Book 特别警告:“同一编译单元中的对象按照定义顺序创建……然而,不同编译单元中静态存储期对象的初始化顺序未被定义……这种未定义行为会导致静态初始化顺序问题(static initialization order problem),当一个静态对象的构造依赖另一个尚未初始化的静态对象时,就会陷入陷阱。由于该惯用法依赖静态存储期对象,它很容易遇到这种问题”​ en.wikibooks.org 。因此,在使用时要避免跨模块依赖静态对象的情况。 此外,由于标准允许延迟静态对象初始化,有时导致该惯用法的可移植性不佳。为此,曾有提案建议在语言层面引入关键字强制全局对象在 main 之前初始化,以提高该惯用法的可移植性​ en.wikibooks.org 。总之,“Attach by Initialization” 是一种强大的初始化技巧,但开发者必须了解静态初始化的规则和潜在风险,在设计时加以注意。 参考资料: 除前述 Wiki Book 内容外,可参见相关 C++ 社区讨论和标准文档(如 ISO WG21 提案)对静态初始化行为的说明​ en.wikibooks.org ​ open-std.org 。 引用 Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version Favicon More C++ Idioms/Print Version - Wikibooks, open books for an open world

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Print_Version ISO/IEC JTC1/SC22/WG21 N0717

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/1995/N0717.htm 全部来源 Faviconen.wikibooks

演示程序

使用这个设计思想,构建一个简单的C++插件系统?我想创建一个简单的C++ 数据结构知识演示程序,每一个数据结构知识点以插件形式注册到该演示主程序。


ChatGPT 说: 当然可以!使用 “Attach by Initialization” 这个 C++ 惯用法,你可以构建一个 自动注册插件系统,在程序启动前就将各个“插件”附着到主程序中。下面是一个完整的简化实现,适合用来构建你提到的“数据结构知识演示程序”。

🧠 核心思想 - 每个“知识点插件”通过构造全局对象,将自身注册到一个插件管理器中。 - 主程序通过遍历注册表来展示或运行每个插件。 - 不需要手动调用注册函数,所有注册在 main() 执行前完成。

✅ 结构说明 我们构建以下几个组件: - Plugin Interface(接口类) - Plugin Registry(全局注册器) - Macros(注册宏) - Example Plugins(如:Stack、Queue、LinkedList) - Main Program(自动列出所有注册的插件并调用)

📁 项目结构(建议)

datastructures/
├── Plugin.h
├── PluginRegistry.h
├── PluginRegistry.cpp
├── StackDemo.cpp
├── QueueDemo.cpp
├── main.cpp

1. Plugin.h:定义插件接口

这个演示的插件类抽象接口,主要就两个,一个返回插件名字,一个演示插件运行效果

// Plugin.h
#pragma once
#include <string>

class Plugin {
public:
    virtual ~Plugin() = default;
    virtual std::string name() const = 0;
    virtual void run() = 0;
};

2. PluginRegistry.h:定义注册器接口和注册宏

这个里面关键的就是插件注册宏,这个是常用的编程技巧

// PluginRegistry.h
#pragma once
#include "Plugin.h"
#include <vector>
#include <memory>

class PluginRegistry {
public:
    static PluginRegistry& instance();

    void registerPlugin(std::unique_ptr<Plugin> plugin);
    const std::vector<std::unique_ptr<Plugin>>& getPlugins() const;

private:
    std::vector<std::unique_ptr<Plugin>> plugins;
};

// 注册宏
#define REGISTER_PLUGIN(CLASS_NAME)                    \
    namespace {                                        \
        struct AutoRegister_##CLASS_NAME {            \
            AutoRegister_##CLASS_NAME() {             \
                PluginRegistry::instance().registerPlugin( \
                    std::make_unique<CLASS_NAME>());  \
            }                                          \
        };                                             \
        static AutoRegister_##CLASS_NAME _auto_##CLASS_NAME; \
    }

3. PluginRegistry.cpp:注册器实现

注册类的实现,使用vector保存单一的插件类,使用unique_ptr管理plugin对象

// PluginRegistry.cpp
#include "PluginRegistry.h"

PluginRegistry& PluginRegistry::instance() {
    static PluginRegistry registry;
    return registry;
}

void PluginRegistry::registerPlugin(std::unique_ptr<Plugin> plugin) {
    plugins.push_back(std::move(plugin));
}

const std::vector<std::unique_ptr<Plugin>>& PluginRegistry::getPlugins() const {
    return plugins;
}

4. 示例插件:StackDemo.cpp

单一的插件演示类,没有具体功能,只是打印做一些演示用

// StackDemo.cpp
#include "Plugin.h"
#include "PluginRegistry.h"
#include <iostream>

class StackDemo : public Plugin {
public:
    std::string name() const override { return "Stack Demonstration"; }
    void run() override {
        std::cout << "This is a simple stack demo.\n";
        std::cout << "Pushing 1, 2, 3 into stack...\n";
        std::cout << "Popping: 3, 2, 1\n";
    }
};

REGISTER_PLUGIN(StackDemo)

另一个插件:QueueDemo.cpp

// QueueDemo.cpp
#include "Plugin.h"
#include "PluginRegistry.h"
#include <iostream>

class QueueDemo : public Plugin {
public:
    std::string name() const override { return "Queue Demonstration"; }
    void run() override {
        std::cout << "This is a simple queue demo.\n";
        std::cout << "Enqueue: A, B, C\n";
        std::cout << "Dequeue: A, B, C\n";
    }
};

REGISTER_PLUGIN(QueueDemo)

5. main.cpp:主程序自动枚举并运行插件

// main.cpp
#include "PluginRegistry.h"
#include <iostream>

int main() {
    const auto& plugins = PluginRegistry::instance().getPlugins();

    std::cout << "📘 Data Structure Demos Registered:\n";
    int index = 1;
    for (const auto& plugin : plugins) {
        std::cout << index++ << ". " << plugin->name() << "\n";
    }

    std::cout << "\n🔧 Running all plugins:\n\n";
    for (const auto& plugin : plugins) {
        std::cout << "==> " << plugin->name() << " <==\n";
        plugin->run();
        std::cout << "-----------------------------\n";
    }

    return 0;
}

🛠️ 编译方式(g++ 或 clang++)

g++ -std=c++17 -o ds_demo main.cpp StackDemo.cpp QueueDemo.cpp PluginRegistry.cpp

./ds_demo ✅ 输出示例

📘 Data Structure Demos Registered:
1. Stack Demonstration
2. Queue Demonstration

🔧 Running all plugins:

==> Stack Demonstration <==
This is a simple stack demo.
Pushing 1, 2, 3 into stack...
Popping: 3, 2, 1
-----------------------------
==> Queue Demonstration <==
This is a simple queue demo.
Enqueue: A, B, C
Dequeue: A, B, C
-----------------------------

🔍 总结:你学到的内容 - 使用全局静态对象实现“自动注册” - 不需要 main 显式调用每个插件 - 未来可以按数据结构类型添加多个文件,支持模块化扩展 - 是教学型 C++ 项目的理想起点

你希望我帮助你把这个系统扩展为 GUI、命令行菜单,或动态插件加载(如 .so / .dll)版本吗?

这个小例子,完整演示了刚才前面提到的attach by initializaton惯用法,里面使用了一些unicode emoji图形,在winows上有些问题

std::cout << "📘 Data Structure Demos Registered:\n"; 里面的图形,在Windows 中文系统输出乱码?


是的,std::cout << "📘 Data Structure Demos Registered:\n"; 这行中的 📘 是一个 Unicode Emoji 字符,在 Windows 中文系统的控制台(CMD / PowerShell) 中默认并不支持完整 Unicode 或 Emoji 渲染,因此你看到的是乱码。

✅ 解决方案选项 ✅ 方案一:改成纯 ASCII 文本(推荐) 为了兼容性,直接用 ASCII 替代:

std::cout << "[INFO] Data Structure Demos Registered:\n";

或者用简单的 ASCII 图形:

std::cout << "[*] Data Structure Demos Registered:\n";

✅ 方案二:设置 Windows 控制台支持 UTF-8(不推荐用于初学者) Windows 控制台默认编码是 GBK(936),你可以改为 UTF-8 编码:

启用 UTF-8 控制台:

#include <windows.h>
SetConsoleOutputCP(CP_UTF8);

编译加上 /utf-8 标志(如果用 MSVC 编译器)或设置源文件为 UTF-8 编码。

控制台字体改为支持 Unicode 的字体(如“Lucida Console” 或 “Consolas”)。

⚠️ 但即使这样,某些 Emoji 图形在 Windows 控制台依然不能正确显示。

使用场景 建议文本
通用跨平台 [INFO]、[*]、-> 等 ASCII
教学/演示幻灯片 Emoji ✅
GUI 项目 可以使用图形界面 Emoji

你想保留视觉提示效果的话,我可以推荐几个兼容性好的 ASCII 替代符号,是否需要?


方案二 的第1步,我已经成功设置,可以正确显示 Unicode Emoji 字符


ChatGPT 说: 很好!既然你已经通过 SetConsoleOutputCP(CP_UTF8); 成功设置了 UTF-8 输出,并能在控制台正确显示 Unicode Emoji 字符,那么你只需注意以下几点,确保跨平台时也保持兼容性:

✅ Emoji 输出完整设置(汇总) 建议将以下代码放在 main() 的开始:

#ifdef _WIN32
#include <windows.h>
#endif
#include <iostream>

int main() {
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8); // 设置控制台为 UTF-8
#endif

    std::cout << u8"📘 Data Structure Demos Registered:\n";
    return 0;
}

🔍 关键点说明:

设置项 描述
SetConsoleOutputCP(CP_UTF8); 让 Windows 控制台改为 UTF-8 编码 (65001)
u8"" 前缀 明确告诉编译器这是一段 UTF-8 字符串
控制台字体 推荐使用支持 Emoji 的字体(如 Consolas)

💡 额外建议(防止乱码): - 源代码文件编码应为 UTF-8 无 BOM。 - 编译器设置确保不自动转码字符串(MSVC 建议加 /utf-8)。 - 避免混用宽字符 wchar_t 与 UTF-8 输出流 std::cout,统一使用 std::string 和 std::cout 更稳定。 - 你是否还希望我帮你封装一个 ConsoleLogger 工具类,用于美化带 Emoji 的输出?

真不错,对于初学者学习编程,真的太友好了,个人觉得现实中已经没有哪个老师能比得上AI的辅助了

评论