C++ Initialization

C/C++ Initialization

问题

在工作项目中,遇到了零初始的相关问题,简单例子:
有以下结构体,并使用make_unique进行对象的堆分配

struct test
{
  int a[1024];
  float b[1024];
};
auto& test1 = std::make_unique<test>();
std::cout << test1->a[0] << " " << test1->b[0] << std::endl;

其结果是'0 0', 表示该结构已被初始化,如果能看到调用栈,不出意料的话应该有memset调用。但有时候,test1作为一个出参,仅仅是被创建出来就可以了,不需要0初始化,它实际的值由后续的流程决定。那么,编译器对test1进行的自动0初始化就是没有必要的,在一些情况下也会导致性能问题。

所以,本篇文章的目的是如何抑制test的0初始化,但是我觉得有必要先整理下C++初始化的规则,最后再探讨如何解决自己的问题。
因此我先查阅了相关资料查阅一番,发现这个视频讲的较好,以下所有理论内容都整理自该视频:
Back to Basics: Initialization in C++ - Ben Saks - CppCon 2023

C初始化规则

C 语言中的类型分类, 不翻译了

Scalars are simple built-in types:

  • integers
  • characters
  • floating-point values
  • pointers
  • enumerations

Aggregates are objects that are made up of some small pieces, such as:

  • structures
  • arrays

Unions are not technically aggregates in C, but some unions are aggregates in C++

Initialize a scalar with a single value(copy-initialization)

C语言中,没有专门的属于描述以上初始化过程,但在C++中我们称之为拷贝初始化(copy-initialization)

int x = 3;
double y = 3.5f;
int *px = &x;

Aggregate Initialization

C语言中我们可以用大括号列表初始化一个聚合体(aggregate), c++称之为"aggregate-initialization":

struct widget {
    int id;
    double price;
}
widget w1 = {1000, 6.5}
int values[3] = {1, 2, 3};

以下代码:w1使用aggregate-initialization, w2则为copy-initialization

struct widget
{
    int id;
    double price;
}

widget w1 = {1000, 6.5}; // aggregate-initialization
widege w2 = w1;			// copy-initialization

Zero Initialization

涉及到两种情况

Objects without explicit initializer

对于一个没有显示初始化器(explicit initializer)的对象, 又分以下两种情况

  1. 如果该对象有静态或者线程存储周期(static or thread storage duration),它将被零初始化(zero-initialization)

    • 即该对象以值0的方式进行初始化

    • 对于聚合类(aggregate), 每个成员变量都以0初始化

    • 指针被初始化为null(即使在一些平台上空指针的值并不一定是0)

  2. 否则,该对象处于一种未初始化的状态,即它的初始化值不确定(由此可能导致undefined behavior)

Partial explicit initializer

如果一个aggregate使用大括号初始化列表进行初始化,但是大括号初始化列表并没有显示指明每个元素的值,那么这个aggregate的其余元素以0初始化(zero-initialized):

struct widget
{
    int id;
    double price;
};

widget w1 = {1000}; // 等价于 {1000, 0.0}
int values[3] = {1}; // 等价于 {1, 0, 0}

C++ 构造函数的起源

大多数C++98/03引入的新初始化规则与 类、构造函数以及生命周期有关

  • 一个对象初始化完成时即是它的生命周期的开始
  • C语言不会区分 storage duration以及lifetime(生命周期)。(storage duration是啥?)

例子如下:

class demo_str
{
public:
    ~~~
private:
    std::size_t stored_length;
    char* actual_str;
}

如果以上demo_str表示一个字符串类,那么这个类有一个“不变量”(class invariant):

  • stored_length < size of the array which actual_str points to

  • 这就是为什么C++需要额外映入新的初始化规则的主要原因

    • 如果只有C语言的aggregate initialization, 如下所示 ds对象就违反了上述类不变量

      demo_str ds = {5, nullptr}; // empty string of length 5?
      
  • 但是如果一个类有用户定义的构造函数,那么这个类就不在是一个aggregate,因此不再支持 aggregate-initialization

C++03 初始化规则

按照C++03的规定,如果一个类满足以下条件,就属于aggregate类:

  • 没有用户定义的构造函数或者继承的构造函数
  • no private or protected direct non-static data members
  • 没有 virtual, private或者protected基类
  • 没有虚函数

Direct Initialization

我们可以在圆括号内写构造函数的参数,direct-initialize一个 demo_str 对象(C 语言不支持这样做):

demo_str ds1 ("Hello", 5); // demo_str(char const *m, std::size_t),保持了类不变量
demo_str ds2 (ds1);        // demo_str(demo_str const&), 也算是direct-initialize,而不是copy-initialize

c++ 致力于一致性地对待user-defined types和built-in types ,因此允许对基础标量类型使用direct-Initialization,如下:

int x(5);
double y(3.5);

Direct Initialization vs. Copy Initialization

还是看上述例子:

demo_str ds1 ("Hello", 5);
demo_str ds2 (ds1); 
demo_str ds3 = ds1;

这里的’demo_str ds2 (ds1); ‘ 使用direct-initialization而不是copy-initialization。 但是ds3的初始化使用的是 Copy Initialization, 因为这里初始化器通过等号指定。

Why care about the difference between direct-initialization and copy-initialization

因为这两个初始化方式的区别会影响允许的转换操作:

  • 一个没有explicit关键词修饰的构造函数是一个(隐式的)转换函数

Default Initialization

省略圆括号后,demo_str就会使用默认构造函数进行初始化,c++称之为 default-initialization:

demo_string ds;

Vacuous Initialization

和C类似,没有初始化器的标量对象实际上没有被初始化

int x; // uninitialized

但是,从c++的理念看,上述代码的x依然是 default-initialized,用来描述x的生命周期的开始。

这就导致了在c++标准中的一些比较让人混淆的术语:

  • 在某些地方,c++标准称 x 变量是未初始化的(uninitialized)
  • 但在其他地方,c++标准又称这是没有做任何事情的初始化方式——“vacuous Initialization”

Value Initialization

在某些情况下,我们可以使用一个显示的圆括号进行初始化

demo_str::demo_str():
	stored_length(),
	actual_str()
{
}

demo_str *pds = new demo_str();

这种初始化方式叫做Value Initialization。

Value-initialize 一个类型为T对象的具体行为,视T的某些具体特性而定:

  • 如果T有一个用户自定义的默认构造函数,调用它
  • 如果T是一个数组,Value-initialize 它的所有元素
  • 如果T是一个标量,执行zero-Initialize
  • 否则如果是聚合体,且只有当T没有用户定义的构造函数时:
    • 对T的每个成员变量
      • 先执行zero-Initialize
      • 然后,再执行default-Initialize,即调用默认构造函数

Areas for improvement in c++03

尽管c++03提供了很多的初始化方式,但依然有一些不足之处

  1. 没有同一的初始化语法

    • c++03有三种类型需要考虑
      • 标量(scaler)
      • 聚合体(aggregate)
      • 有用户自定义构造函数的类
    • 但是没有一种初始化方式对三种类型都适用
      • 标量以及有用户自定义构造函数的类不支持大括号初始化方式
      • 聚合体不支持直接初始化(direct-initialization)
  2. 存在narrowing conversion

    • 如果在初始化过程中使用了可能导致精度损失的转换,C++03不会提供任何保护手段:

      float f = 10000000.5;
      int i = f;	// discard fractional part of f
      short s = i;  // value might not fit in a short integer
      
  3. 容易让人混淆的 value-Initialization :

    demo_str ds1; // default init
    demo_str ds2 (); // declare a function!!
    demo_str ds3 = demo_str(); // value-init/copy-init
    
  4. 不能用初始化数组的方式初始化一个stl容器:

    int values[] = {1, 2, 3};
    std::vector<int> v{1, 2, 3}; // invalid until c++11 !!
    std::vector<int> v(values, values +3); // only allowed this init in c++03
    

现代C++初始化规则

现代c++尝试使用大括号初始化的方式将所有类型的初始化进行统一:

int x{3}; 						// scalar
int arr[3] {1, 4, 9};			// aggregate
demo_str ds {"Hello Word", 5}	// class with constructor

Value-Initialization in Modern Cpp

  1. 空的花括号括号为执行Value-initialization 提供了方便,并且杜绝了空圆括号导致的混淆

    int x; // uninitialized
    int y(); // function declaration!
    int z {};// value_init, z is 0
    int *p1 = new int(); // value-init, *p1 is 0
    int *p2 = new int{}; // value-init, *p2 is 0
    
    demo_str ds1; // default init, call demo_str()
    demo_str ds2 (); // declare a function!!
    demo_str ds3 {}; // value-init/copy-init, call demo_str()
    
  2. 现代c++能够方便的在使用模板的代码中提供初始化方法:

    teplate <typename T>
    void f(T const &t) {
        T obj {}; // definitely not unintialized
    }
    
    • 如果T是一个具有构造函数的自定义class,编译器会调用T的构造函数进行初始化
    • 如果T是一个标量,obj会被执行zero-initialized

Direct-List-Initialization

现代cpp允许使用大括号执行direct-initialization, 称作 direct-list-initialization, 比如:

// both call demo_str(char const*, std::size_t)
demo_str ds1 ("Hello World", 5);
demo_str ds2 {"Hello Word", 5};

Copy-List-Initialization

现代CPP允许使用大括号执行copy-initialization, 称作 copy-list-initialization, 比如:

int x = 5;
int y = {5};
// Both of these call demo_str(char const*)
demo_str ds1 = "Picard";
damo_str ds2 = {"Riker"};

相比于与direct-list-initialization, copy-list-initialization有以下特性 :

  • 不允许隐式转换:

    class C {
      public:
        C(int x, demo_str const& str);
    };
    C c1 {10, "Crusher"}; // OK
    C c2 = {10, "Crusher"}; // Compiler ERROR!
    
  • 不允许向下转型

    int dtoi1 = 12.3; // OK
    int dtoi2 {12.3}; // error: not narrowing allowed
    
    int i = 12;
    char itocl {i}; // error: no narrowing allowed
    float itofl {i}; // error: no narrowing allowed
    

Initializer Lists

C++03允许我们创建一个任意内容的数组

int values[] = {1, 2, 3};

现代C++使用 std::initializer_list<T>允许用户自定类达到相似的初始化效果(std::initializer_list 通常是值传递,但底层实现是“浅拷贝”)

class demo_str {
public:
    demo_str();
    demo_str(char const* str);
    demo_str(char const* str, std::size_t length);
    demo_str(std::initializer_list<char> init);
    ~~~
};

这样可以使用大括号执行精确的字符串初始化:

demo_str ds1 {'D', 'a', 't', 'a'};   // direct-list-init
demo_str ds2 = {'T', 'r', 'o', 'i'}; // copy-list-init

如果一类有std::initializer_list 构造函数以及其他的构造函数, 编译器会更倾向于选择std::initializer_list 构造函数

比如std::vector<T>的构造函数:

std::vector<int> v1 (12); // std::vector<int>::size_tyep , construct a vector with 12 zeros
std::vector<int> v2 {12}; // std::initializer_list<int>, construct a vector whose length is 1 and the only elem is 1

Initialization in Templates

由于潜在的模糊性,在模版中使用大括号初始化并不一定是正确的方式。

using VI = std::vector<int>;
std::unique_ptr<VI> p = std::make_unique<VI>(12, -1);
  • 如果std::make_unique使用direct-initialization,p 指向一个包含12个-1的std::vector

  • 如果std::make_unique使用direct-list-initialization,p指向一个包含1个-1的std::vector

这取决于std::make_unique是怎么实现的,可能有两种实现方式,(区别在于大括号和圆括号)、

  • direct-initialization:

    template<typename T, typename... Args>
    std::unique_ptr<T> make_unique(Args&&... args)
    {
        // direct-initialization
        return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    }
    
  • direct-list-initialization:

    template<typename T, typename... Args>
    std::unique_ptr<T> make_unique(Args&&... args)
    {
        // direct-initialization
        return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
    }
    

实际上,std::make_unique以及标库中的相似功能大多使用direct-initialization,所以 std::make_unique<VI>(12, -1) 创建了一个 指向一个包含12个-1的std::vector 的指针

问题回顾和解决

C++初始化规则就是这些,那么如何解决我的问题?
根据以上的信息,我想到两种方法:

  1. 提供用户自定义函数,抑制0初始化:(C++03_Value-initialization)

    struct test
    {
        test() {} // user-defined constructor
        int a[1024];
        float b[1024];
    };
    auto test1 = std::make_unique<test>();
    
  2. 手动new T 后再传给unique_ptr: (现代C++_Initialization in Templates && C++03_Value-initialization)

    struct test
    {
        int a[1024];
        float b[1024];
    };
    auto test1 = std::unique_ptr<test>(new test);
    

    make_unique将调用new T(), 触发Value Initialization. 但 new T 不会, 也可从cppref上确认这点.

posted @ 2026-03-10 01:01  别杀那头猪  阅读(34)  评论(0)    收藏  举报