这可能会使某些编码人员感到惊讶,而且,令人惊讶的是,如果没有编译器的非标准支持,就无法实现std::vector
。问题本质上在于对原始存储区域执行指针算术的能力。出现在@ShafikYaghmour答案中的论文p0593: Implicit creation of objects for low-level object manipulation清楚地揭示了问题所在,并提出了对该标准的修改,以使诸如容器之类的向量和其他法律级别的编程技术的实现更加容易。
尽管如此,我想知道是否没有解决方案,仅使用语言提供的内容而不使用标准库来实现与std::vector
等效的类型。
目标是在原始存储区域中一个接一个地构造矢量元素,并能够使用迭代器访问这些元素。这等效于std :: vector上的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);
上面的代码中是否存在未定义的行为?
答案 0 :(得分:5)
这个主题正在积极讨论中,我们可以在提案p0593: Implicit creation of objects for low-level object manipulation中看到。这是对这些问题以及为什么不进行更改就无法解决这些问题的扎实讨论。如果您对所考虑的方法有不同的看法或看法,则可以与提案作者联系。
它包含以下讨论:
2.3。动态构造数组
考虑该程序尝试实现类似std :: vector的类型(为简洁起见,省略了许多详细信息):
....
在实践中,此代码可在现有的范围内使用 实现,但根据C ++对象模型,未定义 行为发生在#a,#b,#c,#d和#e点,因为它们试图 对分配的存储区域执行指针算术运算 不包含数组对象。
在#b,#c和#d位置,对char *执行算术运算, 在#a,#e和#f位置,对T *执行算术运算。 理想情况下,解决此问题的方法将使两个计算都没有 定义的行为。
- 方法
以上片段具有一个共同的主题:它们尝试使用从未创建过的对象。确实,程序员认为不需要为它们显式创建对象而拥有一类类型。我们建议识别这些类型,并仔细制定规则,从而消除对显式创建此类对象的需求,而改为隐式创建它们。
使用 adc 联合的方法存在一个问题,我们希望能够通过指针T*
,即通过std::vector::data访问包含的数据。以T*
访问联合会违反strict aliasing rules,因此是未定义的行为。