2023年4月

最近写CMU15-445的时候,project 1里面有个任务类似于写unique_ptr智能指针PageGuard,创建时增加引用计数,销毁时减少引用计数。然后这里有个让人疑惑的地方,就是他要求写移动构造函数。当时我想着,这跟用= default应该没啥区别,后来不放心,还是做了个实验验证下,发现和想象中的还是有些出入。

废话不多说,直接贴代码:

#include <bits/stdc++.h>

using namespace std;

struct A {
    int val = 0;
    string msg = "";
};

int main()
{
    A a1{114514, "1919810"};
    printf("a1{%d,%s}\n", a1.val, a1.msg.c_str());

    A a2(std::move(a1));
    printf("a2{%d,%s}\n", a2.val, a2.msg.c_str());
    printf("a1{%d,%s}\n", a1.val, a1.msg.c_str());

    return 0;
}

然后结果是,a2能成功继承a1的所有数据,但是在move后的a1中,只有std::string的值被清空,而int类型的数据仍然保持了原有值,而非初始化到0。这个结果就与成员变量的平凡性有关了:对于平凡类型,move和直接复制该类型没有任何区别。也就是说,仅仅对于非平凡类型而言,才会存在真实的move动作,并将被move对象重置为设定的某个中间状态——平凡类型要求使用编译器隐式生成的移动构造函数,因而并不会对原有数据进行修改,move之后还是保持原有的值。

由于规定以C语言生成的所有数据结构都是平凡的,我们可以推测指针本身也是一种平凡类型,move后不会被清空。那么这下问题就解决了:要求我们自己实现移动构造函数的原因是,在对象中使用了C指针而非智能指针,并不能通过move的方式来解除被move对象与内存区域的绑定,需要自己手动使用nullptr来重置这些指针,防止它们在预期的生命周期外修改数据,制造未定义行为。

看到MSYS和winlibs两个网站上的gcc都区分了MSVCRT和UCRT版本,这里简单记录一下。

再简单提一下C/C++运行库(C Runtime,CRT)的概念吧,因为所有符合C/C++标准的程序采用的绝大部分基本函数都具有相同的功能(最典型的就是stdio.hmemory.hiostream了),因而将这些常见函数抽象出来做成单独的程序库能够有效节省编译时间,并且对闭源编译器还有隐藏实现的功能等。

然后这里MSVCRT指的是当年Visual C++系列随编译器附带的运行库,也就是说发布一版VC++就会发布一版MSVCRT,那么在动态链接的情况下,目标机器就必须安装对应的运行库。比如说原来最为常见的找不到MSVCP80.dll这类信息,就是因为这种情况产生的,也就是俗称的缺库。

那么UCRT就是微软为了解决这一问题提出的新方案。程序不再链接到某一具体版本的CRT,而是链接到一系列中间dll上,形如api-ms-win-xxxx.dll,想必在某些地方见过。这些dll文件非常小,它们并不包含具体函数的实现,而是作为一个中间层,将对应的函数请求转发到当前系统所支持的CRT中。没有什么是加抽象层解决不了的问题

然后这里区分两种CRT的话,实际上并不只是区分编译出来的程序最终默认链接到哪种CRT,这些gcc本身也会被链接到对应的库。比如说,UCRT版本的gcc会链接到ucrtbase.dll,而MSVCRT版本的会链接到mscvrt.dll

PS1. UCRT版本的gcc包含有MSVCRT的库,但是反过来没有验证是否同样支持; PS2. 在gcc的C支持库中,可以通过传递--with-default-msvcrt=ucrt参数来让MinGW使用UCRT作为其支撑运行库。