C++连续非数组对象指针运算的标准合规性疑问
std::vector实现与指针算术合法性的疑问 通常业界普遍认为,C++20标准正式发布前,开发者无法写出完全合规的std::vector实现(相关讨论可参考标准提案P0593):如果要满足性能要求,就不能将元素直接存放于声明为元素类型的内部数组中,此时对返回的元素指针(尤其是.data()返回的指针)做指针算术运算属于未定义行为,典型的问题示例如下:
char * storage = new char[sizeof(int) * 3]; int * data = new(storage) int(1); new(storage + sizeof(int)) int(2); new(storage + sizeof(int) * 2) int(3); data[2] = 5; // 未定义行为:data指向的地址处不存在int类型数组
但C++17之前的标准文本中,存在如下三条可作为推导依据的条款:
[basic.compound] 若类型为T的对象存储于地址A,则值为地址A的cv限定T*类型指针即指向该对象,与指针值的获取方式无关。
[expr.add] 对于指针加减运算符,指向非数组对象的指针,与指向长度为1、元素类型为该对象类型的数组首元素的指针行为完全一致。
[expr.add] 此外,若表达式P指向某数组对象的最后一个元素,则表达式(P)+1指向该数组对象尾元素的后一位置;若表达式Q指向某数组对象尾元素的后一位置,则表达式(Q)-1指向该数组对象的最后一个元素。
按照上述条款推导,有观点认为如下代码是完全合法、符合标准要求的:
char * storage = new char[sizeof(int) * 3]; int * data = new(storage) int(1); new(storage + sizeof(int)) int(2); new(storage + sizeof(int) * 2) int(3); int * tmp = data + 1; // 依据[expr.add]该操作合法 // 依据[basic.compound],tmp此时指向storage + sizeof(int)位置构造的int对象 // C++17及以上版本需要调用launder进行指针清洗 tmp = tmp + 1; // tmp此时指向storage + sizeof(int) * 2位置构造的int对象 *tmp = 5; (data + 1)[1] = 5; // 与上述操作等价
该推导的核心结论是:对于连续存储的多个同类型对象,只要不直接使用大跨度指针偏移,而是通过逐次步进1个位置的方式移动指针,就可以合法访问相邻的同类型对象。该理解是否符合C++标准的规则本意?
补充说明:C++17标准调整了[basic.compound]和[expr.add]的条款表述,要求每次步进移动指针后都需要对指针做launder清洗,才能正确指向对应位置的int对象,除此之外规则逻辑与此前版本等价。
编辑说明:已移除代码中reinterpret_cast的使用,其使用合法性不属于本问题的讨论范围。
内容的提问来源于stack exchange,提问作者nlsbgr

