是否递增指向未定义的 0 大小动态数组的指针?
Is incrementing a pointer to a 0-sized dynamic array undefined?
据我所知,虽然我们不能创建一个 0 大小的静态内存数组,但我们可以用动态数组来做到这一点:
int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined
正如我所读,p
就像一个尾数元素。我可以打印 p
指向的地址。
if(p)
cout << p << endl;
虽然我确定我们不能取消引用该指针(过去最后一个元素),因为我们不能使用迭代器(过去最后一个元素),但我不确定是否递增那个指针p
?未定义行为 (UB) 是否类似于迭代器?
p++; // UB?
允许指向数组元素的指针指向一个有效的元素,或者指向末尾的元素。如果您以超过结尾的方式递增指针,则行为未定义。
对于大小为 0 的数组,p
已经指向末尾,因此不允许增加它。
参见 C++17 8.7/4 关于 +
运算符(++
具有相同的限制):
f the expression P
points to element x[i]
of an array object x
with n elements, the expressions P + J
and J + P
(where J
has the value j
) point to the (possibly-hypothetical) element x[i+j]
if 0≤i+j≤n; otherwise, the behavior is undefined.
从严格意义上讲,这不是未定义的行为,而是实现定义的。所以,虽然如果你打算支持非主流架构是不可取的,但你可以可能做到。
interjay给的标准引语不错,表示UB,但我认为它只是第二好的命中,因为它涉及指针-指针算法(有趣的是,一个是明确的UB,而另一个不是)。题中有一段直接处理操作:
[expr.post.incr] / [expr.pre.incr]
The operand shall be [...] or a pointer to a completely-defined object type.
哦,等一下,完全定义的对象类型?就这样?我的意思是,真的,type?所以你根本不需要对象?
需要大量阅读才能真正找到其中某些内容可能定义不那么明确的提示。因为到目前为止,它看起来好像完全允许您这样做,没有任何限制。
[basic.compound] 3
声明一个指针可能具有哪种类型,并且作为其他三个指针的 none,您的操作结果显然低于 3.4:无效指针.
然而,它并没有说你不允许有一个无效的指针。相反,它列出了一些非常常见的正常情况(例如存储持续时间结束),其中指针经常变得无效。所以这显然是允许发生的事情。事实上:
[basic.stc] 4
Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.
我们在那里做 "any other",所以它不是未定义的行为,而是实现定义的,因此通常 allowable(除非实现明确说明不同的东西)。
不幸的是,这还没有结束。尽管从这里开始最终结果不再发生任何变化,但搜索 "pointer":
的时间越长,它就会变得越混乱
[basic.compound]
A valid value of an object pointer type represents either the address of a byte in memory or a null pointer. If an object of type T is located at an address A [...] is said to point to that object, regardless of how the value was obtained.
[ Note: For instance, the address one past the end of an array would be considered to point to an unrelated object of the array's element type that might be located at that address. [...]].
读作:好的,谁在乎呢!只要指针指向内存中的某处,我就可以了?
[basic.stc.dynamic.safety]
A pointer value is a safely-derived pointer [blah blah]
读作:OK,安全派生,随便什么。它没有解释这是什么,也没有说我真的需要它。安全派生的。显然我仍然可以拥有非安全派生的指针。我猜取消引用它们可能不是一个好主意,但拥有它们是完全允许的。没有别的说法。
An implementation may have relaxed pointer safety, in which case the validity of a pointer value does not depend on whether it is a safely-derived pointer value.
哦,所以这可能无所谓,只是我的想法。但是等等……"may not"?也就是说,它也可能。我怎么知道?
Alternatively, an implementation may have strict pointer safety, in which case a pointer value that is not a safely-derived pointer value is an invalid pointer value unless the referenced complete object is of dynamic storage duration and has previously been declared reachable
等等,所以我什至可能需要在每个指针上调用 declare_reachable()
?我怎么知道?
现在,您可以转换为 intptr_t
,它定义明确,给出安全派生指针的整数表示。当然,作为一个整数,它是完全合法且定义明确的,可以随心所欲地增加它。
是的,您可以将 intptr_t
转换回指针,这也是定义明确的。只是,不是原始值,不再保证您有一个安全派生的指针(显然)。尽管如此,总而言之,就标准而言,虽然是实现定义的,但这是 100% 合法的事情:
[expr.reinterpret.cast] 5
A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size [...] and back to the same pointer type [...] original value; mappings between pointers and integers are otherwise implementation-defined.
收获
指针就是普通的整数,只是你碰巧把它们当作指针来使用。哦,要是那是真的就好了!
不幸的是,存在 根本不是 真的架构,仅仅生成一个无效指针(不是取消引用它,只是将它放在指针寄存器中)会导致陷阱。
这就是 "implementation defined" 的基础。那,以及随心所欲随时递增指针的事实 可能 当然会导致溢出,这是标准不想处理的。应用程序结束地址space可能与溢出的位置不重合,你甚至不知道特定体系结构上的指针是否存在溢出。总而言之,这是一场噩梦般的混乱,与可能的好处没有任何关系。
另一方面,处理一个过去的对象条件很简单:实现必须简单地确保没有对象被分配,所以地址 space 中的最后一个字节被占用。所以这是明确的,因为它很有用而且保证起来很简单。
我想你已经有了答案;如果你看得更深一点:你说过递增一个尾端迭代器是 UB 因此:这个答案在什么是迭代器?
迭代器只是一个具有指针的对象,递增迭代器实际上是递增它拥有的指针。因此,在许多方面,迭代器是根据指针来处理的。
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; // p points to the first element in arr
++p; // p points to arr[1]
Just as we can use iterators to traverse the elements in a vector, we can use pointers to traverse the elements in an array. Of course, to do so, we need to obtain pointers to the first and one past the last element. As we’ve just seen, we can obtain a pointer to the first element by using the array itself or by taking the address-of the first element. We can obtain an off-the-end pointer by using another special property of arrays. We can take the address of the nonexistent element one past the last element of an array:
int *e = &arr[10]; // pointer just past the last element in arr
Here we used the subscript operator to index a nonexisting element; arr has ten elements, so the last element in arr is at index position 9. The only thing we can do with this element is take its address, which we do to initialize e. Like an off-the-end iterator (§ 3.4.1, p. 106), an off-the-end pointer does not point to an element. As a result, we may not dereference or increment an off-the-end pointer.
这是来自 Lipmann 的 C++ primer 5 版。
所以是UB不要做。
据我所知,虽然我们不能创建一个 0 大小的静态内存数组,但我们可以用动态数组来做到这一点:
int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined
正如我所读,p
就像一个尾数元素。我可以打印 p
指向的地址。
if(p)
cout << p << endl;
虽然我确定我们不能取消引用该指针(过去最后一个元素),因为我们不能使用迭代器(过去最后一个元素),但我不确定是否递增那个指针
p
?未定义行为 (UB) 是否类似于迭代器?p++; // UB?
允许指向数组元素的指针指向一个有效的元素,或者指向末尾的元素。如果您以超过结尾的方式递增指针,则行为未定义。
对于大小为 0 的数组,p
已经指向末尾,因此不允许增加它。
参见 C++17 8.7/4 关于 +
运算符(++
具有相同的限制):
f the expression
P
points to elementx[i]
of an array objectx
with n elements, the expressionsP + J
andJ + P
(whereJ
has the valuej
) point to the (possibly-hypothetical) elementx[i+j]
if 0≤i+j≤n; otherwise, the behavior is undefined.
从严格意义上讲,这不是未定义的行为,而是实现定义的。所以,虽然如果你打算支持非主流架构是不可取的,但你可以可能做到。
interjay给的标准引语不错,表示UB,但我认为它只是第二好的命中,因为它涉及指针-指针算法(有趣的是,一个是明确的UB,而另一个不是)。题中有一段直接处理操作:
[expr.post.incr] / [expr.pre.incr]
The operand shall be [...] or a pointer to a completely-defined object type.
哦,等一下,完全定义的对象类型?就这样?我的意思是,真的,type?所以你根本不需要对象?
需要大量阅读才能真正找到其中某些内容可能定义不那么明确的提示。因为到目前为止,它看起来好像完全允许您这样做,没有任何限制。
[basic.compound] 3
声明一个指针可能具有哪种类型,并且作为其他三个指针的 none,您的操作结果显然低于 3.4:无效指针.
然而,它并没有说你不允许有一个无效的指针。相反,它列出了一些非常常见的正常情况(例如存储持续时间结束),其中指针经常变得无效。所以这显然是允许发生的事情。事实上:
[basic.stc] 4
Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.
我们在那里做 "any other",所以它不是未定义的行为,而是实现定义的,因此通常 allowable(除非实现明确说明不同的东西)。
不幸的是,这还没有结束。尽管从这里开始最终结果不再发生任何变化,但搜索 "pointer":
的时间越长,它就会变得越混乱[basic.compound]
A valid value of an object pointer type represents either the address of a byte in memory or a null pointer. If an object of type T is located at an address A [...] is said to point to that object, regardless of how the value was obtained.
[ Note: For instance, the address one past the end of an array would be considered to point to an unrelated object of the array's element type that might be located at that address. [...]].
读作:好的,谁在乎呢!只要指针指向内存中的某处,我就可以了?
[basic.stc.dynamic.safety] A pointer value is a safely-derived pointer [blah blah]
读作:OK,安全派生,随便什么。它没有解释这是什么,也没有说我真的需要它。安全派生的。显然我仍然可以拥有非安全派生的指针。我猜取消引用它们可能不是一个好主意,但拥有它们是完全允许的。没有别的说法。
An implementation may have relaxed pointer safety, in which case the validity of a pointer value does not depend on whether it is a safely-derived pointer value.
哦,所以这可能无所谓,只是我的想法。但是等等……"may not"?也就是说,它也可能。我怎么知道?
Alternatively, an implementation may have strict pointer safety, in which case a pointer value that is not a safely-derived pointer value is an invalid pointer value unless the referenced complete object is of dynamic storage duration and has previously been declared reachable
等等,所以我什至可能需要在每个指针上调用 declare_reachable()
?我怎么知道?
现在,您可以转换为 intptr_t
,它定义明确,给出安全派生指针的整数表示。当然,作为一个整数,它是完全合法且定义明确的,可以随心所欲地增加它。
是的,您可以将 intptr_t
转换回指针,这也是定义明确的。只是,不是原始值,不再保证您有一个安全派生的指针(显然)。尽管如此,总而言之,就标准而言,虽然是实现定义的,但这是 100% 合法的事情:
[expr.reinterpret.cast] 5
A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size [...] and back to the same pointer type [...] original value; mappings between pointers and integers are otherwise implementation-defined.
收获
指针就是普通的整数,只是你碰巧把它们当作指针来使用。哦,要是那是真的就好了!
不幸的是,存在 根本不是 真的架构,仅仅生成一个无效指针(不是取消引用它,只是将它放在指针寄存器中)会导致陷阱。
这就是 "implementation defined" 的基础。那,以及随心所欲随时递增指针的事实 可能 当然会导致溢出,这是标准不想处理的。应用程序结束地址space可能与溢出的位置不重合,你甚至不知道特定体系结构上的指针是否存在溢出。总而言之,这是一场噩梦般的混乱,与可能的好处没有任何关系。
另一方面,处理一个过去的对象条件很简单:实现必须简单地确保没有对象被分配,所以地址 space 中的最后一个字节被占用。所以这是明确的,因为它很有用而且保证起来很简单。
我想你已经有了答案;如果你看得更深一点:你说过递增一个尾端迭代器是 UB 因此:这个答案在什么是迭代器?
迭代器只是一个具有指针的对象,递增迭代器实际上是递增它拥有的指针。因此,在许多方面,迭代器是根据指针来处理的。
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; // p points to the first element in arr
++p; // p points to arr[1]
Just as we can use iterators to traverse the elements in a vector, we can use pointers to traverse the elements in an array. Of course, to do so, we need to obtain pointers to the first and one past the last element. As we’ve just seen, we can obtain a pointer to the first element by using the array itself or by taking the address-of the first element. We can obtain an off-the-end pointer by using another special property of arrays. We can take the address of the nonexistent element one past the last element of an array:
int *e = &arr[10]; // pointer just past the last element in arr
Here we used the subscript operator to index a nonexisting element; arr has ten elements, so the last element in arr is at index position 9. The only thing we can do with this element is take its address, which we do to initialize e. Like an off-the-end iterator (§ 3.4.1, p. 106), an off-the-end pointer does not point to an element. As a result, we may not dereference or increment an off-the-end pointer.
这是来自 Lipmann 的 C++ primer 5 版。
所以是UB不要做。