【问题标题】:Implementing a std::vector like container without undefined behavior在没有未定义行为的情况下实现类似 std::vector 的容器
【发布时间】:2019-03-30 11:56:57
【问题描述】:

这可能会让一些编码人员感到惊讶,而且,如果没有编译器的非标准支持,就不可能实现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);

上面这段代码中是否有任何未定义的行为?

【问题讨论】:

  • 什么是法律级编程???
  • @YunfeiChen 在 C++ 中,几乎只使用语言原语。高级编程是你只使用详细的抽象库

标签: c++ memory-management language-lawyer undefined-behavior


【解决方案1】:

这是正在积极讨论的话题,我们可以在提案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* 执行算术运算。 理想情况下,该问题的解决方案将在两个计算中都包含 定义的行为。

  1. 接近

上面的 sn-ps 有一个共同的主题:他们试图使用他们从未创建过的对象。实际上,程序员认为他们不需要显式创建对象的一系列类型。我们建议识别这些类型,并仔细制定规则,无需显式创建此类对象,而是隐式创建它们。

使用 adc 联合的方法存在一个问题,即我们希望能够通过指针 T* 即通过 std::vector::data 访问包含的数据。以T* 访问联合将违反strict aliasing rules,因此是未定义的行为。

【讨论】:

  • 你不认为我提出的技巧可以解决分配存储上的指针运算问题,还是我错过了一个未定义的行为?
  • @Oliv 我相信你有strict aliasing issues,因为向量的底层应该作为存储的数据类型访问,即通过data() 方法
  • 在编写第一个 C 和 C++ 标准时,作者没有理由认为有人会关心它是否真正定义了明显有用且一致支持的构造的行为。因此,他们采用了行为模型,这些模型只有通过忽略某些必要的操作被标记为 UB 的事实才能变得“可行”。不幸的是,由于未能明确认识到不同的实现用于不同的目的,其中一些需要支持比其他更多的操作,标准已经分裂了语言......
  • ...分为两类方言,一类不适合涉及低级编程的目的,另一类只支持有限的优化。根据 C Rationale,C 精神包括“不要阻止程序员做需要做的事情”的原则。基于程序员不会做 X 的想法进行优化仅在程序员不需要做 X 的情况下才有帮助。不幸的是,大多数关于已定义行为的争论完全忽略了这一点。
猜你喜欢
  • 2014-02-12
  • 1970-01-01
  • 2023-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-18
  • 1970-01-01
  • 2022-07-10
相关资源
最近更新 更多