一、初始化
初始化这个动作,对于开发者来说是最熟悉不过的了。不管是哪种语言,都需要对应用目标进行初始化。初始化是为目标提供一个显式的默认行为。这个行为可以是一个值、一个对象或者一个具体的操作等。在初始化中,最常见的当然是变量的初始化。从全局、静态再到局部变量。都是在编程过程中经常遇到的现象。但是如何正确的初始化,这就是一个问题了。整不好,就会出各种问题。
需要提醒的是,这里的"统一初始化",不是C++标准的"统一初始化",只是字面意思,统一到一个地方进行初始化。
二、初始化的重要性
初始化的重要性,有过相当开发经验或者踩过这类坑的开发者当然最清楚。在前面的文章中分析过动态库中全局变量的依赖性初始化,利用新的c++11后的新功能如何初始化,对列表初始化的各种分析等等。同时也分析过使用C++20提供的constinit避免SIOF(静态初始化顺序)。以上这些等等,都说明了,C++标准本身对如何协调和保障正确的初始化都在不断的进行着完善。
初始化如此重要,原因何在呢?
- 避免UB行为
这是最典型的原因之一,特别是未初始化的局部变量,往往其可能有着内存中的随机值或上一次的其它变量的值。这可能导致未知的行为,而未知是风险的重要来源 - 一致性保障
不管是变量还是其它操作行为,相同的情况下必须提供相同的结果,这是基础的要求。但相同的类提供了不同的构造函数,当在使用时,未显示的进行初始化。可能导致同个类的对象在不同的情况下产生不同的结果。这会让调试变得复杂和困难 - 确保数据的正确性
对于一些const或引用成员,如果没有初始化可能会导致编译的错误。而如果在一些变量中有着依赖行为,未初始化的依赖项可能隐式的导致变量的初始化错误 - 提高性能
典型的就是类成员中使用列表初始化可以提高性能。而在高版本的标准中,使用编译期的初始化可以避免动态初始化的开销,并有可能避免SIOF问题。而初始化的行为可以保证程序工作状态的稳定安全,进而提高整体的性能 - 增加代码的可读性和可维护性
自从C++11后提供了统一初始化后,对变量初始化提供了显式的统一的初始化方式,可读性更高。而对于其它行为,可以采用统一初始化或集中注册管理的方式等来实现初始化的全面接管,利用"损害集中制"的原则来增加可维护性。也就是说,统一的初始化意味着相关代码的高内聚,便于代码的阅读和后期管理
需要特别提醒的是,有些情况下,初始化会自动完成。这时就要区分开零初始化和默认初始化。一般来说,全局或静态变量会自动进行零初始化。但局部变量的默认初始化往往是随机值。特别是对基础类型,更是如此。所以在实际应用时,一定要小心。
三、初始化分类
一般来说,对于C++的初始化。除了沿袭了C语言中的初始化的方式外,还引入了构造函数等初始化的方式。特别是随着新的C++标准的不断迭代,各种初始化方式也不断的推出。它除了让初始化更方便、安全的实现外,还让初始化从设计让就变得更容易操作。一般来说,可以把初始化分成三大情况:
- 传统的初始化
这种一般是使用C++11前的各种初始化方式,如基础类型的直接赋值;类成员变量在构造函数中的列表初始化,const变量的定义即赋值等等以及RAII这种初始化的方式。对于操作行为则提供了类似init()函数机制的问题 - 现代C++初始化
这个在前面分析过很多,最典型的就是std::initializer_list和统一初始化即直接使用{}来进行各种常见的变量初始化,前面分析过的就地初始化(launder和start_lifetime_as)、移动语义及constinit等等;对于操作行为则可以使用一些关键字如构造函数的default等等。 - 编译期初始化
这种在模板或元编程中很常见,即把各种初始化挪移到编译期,这也算是一种初始化的类型吧。
这些初始化过程中,往往有很多的细节和技巧可以展开和应用,这就导致开发者的相同的初始化目的,却可以有着很大的代码实现的不同。这也是经常说的多看优秀的代码的主要原因。
四、具体的方法和技巧
一般来说,初始化可以采用两种机制:
-
自动初始化
这种初始化除了借助一些编译器或运行时机制的处理外。还可以使用用下面的机制:- 使用RAII:这个是经典的解决初始化的机制,不再展开
ctemplate <typename T> class AutoRegister { public: AutoRegister(const std::string& name) : name_(name) { bool success = MsgFactory::instance().regCreator(name_, []() { return std::make_unique<T>();}); } }; static AutoRegister<Log> log("txt"); // 静态变量- 使用gcc的构造属性:这个可能多数开发者用的不多,即使用__attribute__((constructor))(gcc/clang)。而在MSVC中则可以使用其它的机制如".CRT$XCU"等。
c// __attribute__((constructor)) __attribute__((constructor)) static void attrConstructorDemo() { Initializer::Instance().Register({ std::cout<<"demo register!"<<std::endl; static AutoRegister<Log> log("txt"); static AutoRegister<Data> data("json"); }); }说明:__attribute__构造是有优先级可设定的,类似于"void init_sys() attribute((constructor(101)));"
-
统一调用初始化
在前面的全局变量初始化依赖,提出过统一指定到文件中进行统一初始化的机制。同样,对于其它行为的初始化,如句柄、内存等的初始化,同样可以使用一个统一的注册的初始化函数进行处理。防止出现互相依赖和顺序不同导致的各种异常问题。尤其是对于一些指针的初始化,更是如此。这种统一的机制,有很多种方法,如传统的宏(MFC源码)、设计模式和反射或者是几者的统一结合。这个有兴趣可以翻一下前面的反射相关的实现中有类似的代码。
看下面的代码:c#define REGISTER_DB_DRIVER(DriverClass, DriverName) \ namespace { \ struct DriverClass##_Registrar { \ DriverClass##_Registrar() { \ DatabaseFactory::getInstance().registerDriver(DriverName, []() -> std::unique_ptr<IDatabase> { \ return std::make_unique<DriverClass>(); \ }); \ } \ }; \ static DriverClass##_Registrar g_##DriverClass##_registrar; \ }这段代码来自网上的数据库实现中的注册初始化的宏方式实现。有兴趣可以去网查看完整的实现的例程。
五、现代c++初始化的发展
总体上来看,C++初始化统一朝着一个方向在前进。就是让初始化变得清晰、直白、简单且更容易维护。在这其中,一般有以下几个显式的要求:
-
防止转换引起窄化
这是对传统的强制转换的一种限制,防止出现将高精度值赋值给低精度值时数据截断。如下面的例子:cint pi {3.1415};//编译错误 int pi = 3.14; //警告 -
消除二义性
这个最简单直观的例子就是面试中经常问题的,对于无参构造函数对象构造时为什么没有小括号,即:cclass Foo{}; Foo a;//Foo a();另外,还可以区分拷贝初始化和赋值初化(即拷贝构造函数和重载=号)。这都是基础但很容易犯错误的情况。
-
通用性
这个非常好理解,原来数组、容器、类以及结构体等的初始化各有各的不同,但在高版本的标准中,提供了统一初始化的机制。这样就弥合了这些初始化方式的差别。让初始化变得统一化、清晰化。让代码变得更容易编写和理解
明白了上述的基本要求,其实再看标准的演进,就会发现一条清晰的脉络。掌握这个脉络,就明白了c++代码未来轮廓。
六、例程
下面看一个例程:
c
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
// 统一接口。
class Component {
public:
virtual ~Component() = default;
virtual void init() = 0;
};
class RegContaion {
public:
using Factory = std::function<std::unique_ptr<Component>()>;
static RegContaion &instance() {
static RegContaion RegContaion;
return RegContaion;
}
bool add(std::string name, Factory factory) {
auto [it, inserted] = factories_.emplace(std::move(name), std::move(factory));
return inserted;
}
void initializeAll() {
for (const auto &[name, factory] : factories_) {
auto component = factory();
std::cout << "initializing: " << name << std::endl;
component->init();
components_.push_back(std::move(component));
}
}
private:
RegContaion() = default;
std::unordered_map<std::string, Factory> factories_;
std::vector<std::unique_ptr<Component>> components_;
};
class Registrar {
public:
Registrar(std::string name, RegContaion::Factory factory) {
if (!RegContaion::instance().add(std::move(name), std::move(factory))) {
std::cerr << "mul reg..." << std::endl;
}
}
};
#define REGISTER_COMPONENT(name, type) static Registrar registrar_##type(name, []() -> std::unique_ptr<Component> { return std::make_unique<type>(); })
class Network final : public Component {
public:
void init() override { std::cout << "Network initialized" << std::endl; }
};
class Logger final : public Component {
public:
void init() override { std::cout << "Logger initialized" << std::endl; }
};
REGISTER_COMPONENT("network", Network);
REGISTER_COMPONENT("logger", Logger);
int main() {
std::cout << "start reg...!" << std::endl;
RegContaion::instance().initializeAll();
std::cout << "reg end!" << std::endl;
return 0;
}
在实践的应用中,如果是多个静态或全局的注册,需要警惕静态初始化顺序的问题,不要出现循环依赖的情况。如果是一些特殊情况,比如库的初始化、dlopen的动态加载初始化等,还需要处理一些这些特定场景下的细节问题。大家可查阅相关的资料,限于篇幅就不再展开了。
七、总结
整体上来看,初始化的重要性已经得到几乎所有开发者的重视。但这种重视,又会表现在各自的代码开发上,从而可以看出这种重视是停留在表面还是真正的发自内心。但初始化又是一个细节问题,它引起错误的概率又要小得多。所以,是否真正的重视初始化,不仅仅要看代码,还要看设计。从设计的角度去屏蔽开发者不初始化的显式的控制,这才是一个好的架构的开始。