一、大括号的初始化
做为使用大括号进行初始化即使用std::initializer_list进行列表初始化时,会让整个程序看起来更加的清晰明了,实现也相对简单。但这种实现的过程中,会出现各种情况。虽然在整体上,都会生成一个临时的数组来进行数据的存储,但在不同的情况下,C++对其实现的细节也各有不同。随着C++标准的演进,有些实现已经可以进一步的进行优化。本文介绍的静态存储即是如此。
二、存在的问题
在上面提到的不同的情况会有一些不同的处理机制,那么可以对下面的两种实现进行一下对比:
struct X {
X(std::initializer_list<double> v);
};
X x{ 1,2,3 };
这种情况下,对其的展开其实就是将相关的数组数据(1,2,3)存储在静态存储区中。这种情况下,效率和内存应用就相对要好很多。但再看一下这个例子:
void f(std::initializer_list<int> il);
void g() {
f({1,2,3});
}
int main() { g(); }
在这种情况下,则不一定将展开的临时数组存储在静态区域中,重点看f函数的实现,比如下面的代码:
void g();
void f(std::initializer_list<int> il) {
static const int *ptr = nullptr;
if (ptr == nullptr) {
ptr = il.begin();
g();
} else {
assert(ptr != il.begin());
}
}
这是因为标准(C++23及以前)要求两次调用(第一次调用f和在g函数中回调再次调用f)时,其应该具备不同的地址,所以此时两次展开的临时数组(1,2,3)则不能够存储在静态存储中。同样,如果在类似于STL中的容器实现中也有类似的情况:
std::vector<int> vec = {6,7,8,9,10};
这种情况下,数据会进行两次拷贝,即从静态存储到栈,然后再从栈到堆。如果说上面的这种情况,数据量小的情况下,代价并不大。但如果有开发者想在向量中存储一个图像数据怎么办?
三、如何解决
针对上面的问题,在C++26版本前,可以使用前面学习过的#embed方法来嵌入相关的二进制数据,防止数据的拷贝,如:
static const char backing[] = {
#embed "2mb-image.png"
};
std::vector<char> v = std::vector<char>(backing, std::end(backing));
而在C++26中提议使用:
std::initializer_list<int> i1 = {
#embed "very-large-file.png" // 可以
};
void f2(std::initializer_list<int> ia,
std::initializer_list<int> ib) {
PERMIT(ia.begin() == ib.begin()); // 允许 ia.begin() == ib.begin()
}
int main() {
f2({1,2,3}, {1,2,3});
}
//或者如下面的代码
const char *p1 = "hello world";
const char *p2 = "world";
PERMIT(p2 == p1 + 6); // 当前的行为
std::initializer_list<int> i1 = {1,2,3,4,5};
std::initializer_list<int> i2 = {2,3,4};
PERMIT(i1.begin() == i2.begin() + 1); // 提议的未来的行为
其实就是新标准希望常量初始化的初始化列表避免占用栈空间。也就是说,在列表初始化时,不管原来多少次的数组拷贝,都可以被公共使用,这样就会减少拷贝的次数,大大提高了效率。
四、总结
其实对开发者来说,本文更重要的是对C++标准实现的编译内部细节的说明。其实它也是对C++安全和效率机制的一种更新,可能这样做对开发者的影响并不多大。毕竟,很多程序员自己在写代码时对内存的浪费和多次拷贝并不多敏感。但从标准的角度来看,应用开发者可以自由处理,但底层的基础建筑可不能这样,否则就显得有点水平跟不上了。
正如前面所讲,开发者学习的最好的资料和例子就是标准的实现,他们才是最接近于C++标准完美实现的榜样。

793

被折叠的 条评论
为什么被折叠?



