【问题标题】:Why pointer subtraction is undefined behavior in C++?为什么指针减法在 C++ 中是未定义的行为?
【发布时间】:2017-12-19 16:51:00
【问题描述】:

对于下面的例子,什么可能导致undefined behavior为什么?

#include <cstddef> 
#include <iostream> 

template <typename Ty> 
bool in_range(const Ty *test, const Ty *r, size_t n) 
{ 
    return 0 < (test - r) && (test - r) < (std::ptrdiff_t)n; 
}

void f() { 
     double foo[10]; 
     double *x = &foo[0]; 
     double bar; 
     std::cout << std::boolalpha << in_range(&bar, x, 10);
}

我在When is pointer subtraction undefined in C?没有找到答案

【问题讨论】:

  • 应该已经在您引用的问题中找到了答案,因为 C++ 的答案与 C 的答案相同,并且该答案在那里提供。

标签: c++ pointers


【解决方案1】:

指针算术,包括两个指针的减法,只有当指针指向同一个数组中的元素,或者超过该数组末尾的元素时才被定义。在这种情况下,标量算作大小为 1 的数组。

在任何其他情况下允许指针算术是毫无意义的。这样做会不必要地限制 C 的内存模型,并可能降低其灵活性和移植到奇异架构的能力。

【讨论】:

  • 当然,推论是像 OP 提供的 in_range() 函数几乎完全没有意义。您需要已经知道答案才能安全地调用该函数。
  • @coderredoc Stackoverflow 删除了对问题排序非常有用的最后一个测试版,但stackoverflow.com/questions/tagged/… 对我来说已经足够了。活动问题重新组合新的和更新的问题以及相关标签的新的和更新的答案。你只需要等待看到一个告诉你有新事件的栏并点击它。
  • @Stargateur:就是这个!
  • 在任何其他实例(双关语)中允许指针算术是非常有意义的(呃)
  • @Stargateur.: 如果我跟着这个我会很快得到新的问题吗?
【解决方案2】:

对于代码,正如您编写的那样,C++ 的答案与 C 的答案基本相同:当且仅当所涉及的两个指针引用同一数组的一部分时,您才会得到定义的行为,或者一个结束(正如@bathsheba 已经指出的那样,非数组对象被视为与一个项目的数组相同)。

然而,C++ 确实添加了一个可能有用的问题:即使在应用于这样做需要从&lt;functional&gt; 指向分离对象std::less&lt;T&gt; 和朋友的指针。所以,给定两个像这样的独立对象:

Object a;
Object b;

...将两个对象的地址与比较对象进行比较必须“产生一个严格的总顺序,该顺序在这些特化之间是一致的,并且也与内置运算符 、=。” (N4659,[比较]/2)。

因此,您可以像这样编写函数:

template <typename Ty> 
bool in_range(const Ty *test, const Ty *begin, const Ty *end) 
{ 
    return std::less_equal<Ty *>()(begin, test) && std::less<Ty *>()(test, end);
}

如果你真的想保持原来的函数签名,你也可以这样做:

template <typename Ty> 
bool in_range(const Ty *test, const Ty *r, size_t n) 
{ 
    auto end = r + n;
    return std::less_equal<Ty *>()(r, test) && std::less<Ty *>()(test, end);
}

[请注意,我使用std::less_equal 编写第一个比较,使用std:less 编写第二个比较,以匹配通常预期的C++ 语义,其中范围定义为[begin, end)。 ]

这确实带有一个附带条件:您需要确保 r 指向至少包含 n1 的数组的开头,否则 auto end = r + n; 将产生未定义的行为。

至少对于我所期望的这种函数的典型用例,您可以稍微简化使用,但通过传递数组本身,而不是指针和显式长度:

template <class Ty, size_t N>
bool in_range(Ty (&array)[N], Ty *test) {
    return  std::less_equal<Ty *>()(&array[0], test) && 
            std::less<Ty *>()(test, &array[0] + N);
}

在这种情况下,您只需传递数组的名称和要测试的指针:

int foo[10];
int *bar = &foo[4];

std::cout << std::boolalpha << in_range(foo, bar) << "\n"; // returns true

这个只有支持测试实际数组。如果您尝试将非数组项作为第一个参数传递,它将无法编译:

int foo[10];
int bar;
int *baz = &foo[0];
int *ptr = new int[20];

std::cout << std::boolalpha << in_range(bar, baz) << "\n"; // won't compile
std::cout << std::boolalpha << in_range(ptr, baz) << "\n"; // won't compile either

前者可能会防止一些事故。后者可能不太理想。如果我们想同时支持两者,我们可以通过重载来实现(对于所有三种情况,如果我们选择的话):

template <class Ty, size_t N>
bool in_range(Ty (&array)[N], Ty *test) {
    return  std::less_equal<Ty *>()(&array[0], test) &&
            std::less<Ty *>()(test, &array[0]+ N);
}

template <class Ty>
bool in_range(Ty &a, Ty *b) { return &a == b; }

template <class Ty>
bool in_range(Ty a, Ty b, size_t N) {
    return std::less_equal<Ty>()(a, b) && 
           std::less<Ty>()(b, a + N);
}

void f() { 
     double foo[10]; 
     double *x = &foo[0]; 
     double bar;
     double *baz = new double[20];

     std::cout << std::boolalpha << in_range(foo, x) << "\n";
     std::cout << std::boolalpha << in_range(bar, x) << "\n";
     std::cout << std::boolalpha << in_range(baz, x, 20) << "\n";
}

1. 如果你想获得真正的技术,它不必指向数组的开头——它只需要指向数组中至少有n 项的数组的一部分。

【讨论】:

  • 不应该是std::less_equal&lt;Ty*&gt;()(begin, test)(注意星号)吗?
  • @qbolec:是的,至少在大多数情况下是这样。我已经编辑过了。谢谢。
【解决方案3】:

这种情况下的未定义行为通常不会导致崩溃,而是会导致无意义或不一致的结果。

在大多数现代架构中,减去 2 个不相关的指针只是计算地址差除以所指向类型的大小,大致如下:

    A *p1, *p2;
    ...
    ptrdiff_t diff = ((intptr_t)p2 - (intptr_t)p1) / (intptr_t)sizeof(*p1);

出现意外行为的架构示例是英特尔的 16 位分段中型和大型模型:

  • 在 386 及其 32 位模型出现之前,这些模型曾经在 PC 上流行。
  • 远指针存储在两部分中:一个 16 位段(或处于保护模式的选择器)和一个 16 位偏移量。
  • 比较 2 个指针是否相等需要 2 个单独的比较指令以及段和偏移量的条件跳转。
  • 比较指向NULL 的指针通常被优化为段部分与0 的单一比较
  • 仅在偏移部分执行减去 2 个指针并比较相对位置,假设两个指针都指向同一个数组,因此具有相同的段。
  • 在你的例子中,两个对象都有自动存储,所以它们都在同一个段中,指向SS,但是对于从堆中分配的2个对象,你可以同时拥有p &lt;= q &amp;&amp; q &lt;= pp != q时间,或者 p - q == 0p != q 被未定义的行为所覆盖。

【讨论】:

    猜你喜欢
    • 2012-08-19
    • 1970-01-01
    • 1970-01-01
    • 2011-10-11
    • 1970-01-01
    • 1970-01-01
    • 2016-01-22
    相关资源
    最近更新 更多