在没有未定义行为的情况下实现类似std::vector的容器

Implementing a std::vector like container without undefined behavior

本文关键字:std vector 情况下 未定义 实现      更新时间:2023-10-16

这可能会让一些程序员感到惊讶,尽管令人惊讶,但如果没有编译器的非标准支持,就不可能实现std::vector。问题本质上在于对原始存储区域执行指针运算的能力。文章p0593:ShafikYaghmour答案中出现的用于低级对象操作的对象的隐式创建,清楚地暴露了问题,并提出了对标准的修改,以使类向量容器和其他法律级编程技术的实现更容易。

尽管如此,我想知道是否没有办法只使用该语言提供的内容而不使用任何标准库来实现等效于std::vector的类型。

目标是在原始存储区域中一个接一个地构造矢量元素,并能够使用迭代器访问这些元素。这相当于std::向量上的push_back序列。

为了了解这个问题,请简化在libc++或libstdc++中对std::vector的实现执行的操作:

void access_value(std::string x);
std::string s1, s2, s3;
//allocation
auto p=static_cast<std::string*>(::operator new(10*sizeof(std::string)));
//push_back s1
new(p) std::string(s1);
access_value(*p);//undefined behavior, p is not a pointer to object
//push_back s2
new(p+1) std::string(s2);//undefined behavior
//, pointer arithmetic but no array (neither implicit array of size 1)
access_value(*(p+1));//undefined behavior, p+1 is not a pointer to object
//push_back s2
new(p+2) std::string(s3);//undefined behavior
//, pointer arithmetic but no array
access_value(*(p+2));//undefined behavior, p+2 is not a pointer to object

我的想法是使用一个从不初始化其成员的联合。

//almost trivialy default constructible
template<class T>
union atdc{
char _c;
T value;
atdc ()noexcept{ }
~atdc(){}
};

原始存储将使用此联合类型的数组进行初始化,并且指针运算始终在此数组上执行。然后,在每次push_back时,在并集的非活动成员上构造元素。

std::string s1, s2, s3;
auto p=::operator new(10*sizeof(std::string));
auto arr = new(p) atdc<std::string>[10];
//pointer arithmetic on arr is allowed
//push_back s1
new(&arr[0].value) std::string(s1); //union member activation
access_value(arr[0].value);
//push_back s2
new(&arr[1].value) std::string(s2);
access_value(arr[1].value);
//push_back s2
new(&arr[2].value) std::string(s2);
access_value(arr[2].value);

上面的代码中有没有未定义的行为?

这是一个正在积极讨论的主题,我们可以在提案p0593中看到这一点:为低级对象操作隐式创建对象。这是对这些问题的一次相当扎实的讨论,以及为什么不改变这些问题就无法解决。如果你对正在考虑的方法有不同的方法或强烈的看法,你可能想联系提案作者。

其中包括以下讨论:

2.3.阵列的动态构建

考虑一下这个程序,它试图实现一个类似std::vector的类型(为了简洁起见,省略了许多细节(:

在实践中,此代码适用于一系列现有的实现,但根据C++对象模型,未定义行为发生在点#a、#b、#c、#d和#e,因为它们尝试对分配的存储区域执行指针运算不包含数组对象。

在位置#b、#c和#d,对字符*执行算术,并且在位置#a、#e和#f处,对T*执行运算。理想情况下,这个问题的解决方案会给两个计算注入定义的行为。

  1. 方法

上面的代码段有一个共同的主题:它们试图使用从未创建过的对象。事实上,有一个类型家族,程序员认为他们不需要显式地创建对象。我们建议识别这些类型,并仔细制定规则,通过隐式创建来消除显式创建此类对象的必要性。

使用adc并集的方法存在问题,我们希望能够通过指针T*访问包含的数据,即通过std::vector::data。作为T*访问并集将违反严格的别名规则,因此是未定义的行为。