为什么抛出指向原始类的指针的奇怪行为?

iam*_*ind 6 c++ reference strict-aliasing void-pointers reinterpret-cast

假设在我的代码中,我必须存储一个void*as数据成员,并class在需要时将其强制转换回原始指针.为了测试它的可靠性,我写了一个测试程序(linux ubuntu 4.4.1 g ++ -04-Wall),我很震惊地看到了这个行为.

struct A
{
  int i;
  static int c;
  A () : i(c++) { cout<<"A() : i("<<i<<")\n"; }
};
int A::c;

int main ()
{
  void *p = new A[3];  // good behavior for A* p = new A[3];
  cout<<"p->i = "<<((A*)p)->i<<endl;
  ((A*&)p)++;
  cout<<"p->i = "<<((A*)p)->i<<endl;
  ((A*&)p)++;
  cout<<"p->i = "<<((A*)p)->i<<endl;
}
Run Code Online (Sandbox Code Playgroud)

这只是一个测试程序; 实际上,对于我的情况,必须将任何指针存储为void*,然后将其转换回实际指针(借助于template).所以我们不要担心这一部分.上面代码输出是,

p->i = 0
p->i = 0 // ?? why not 1
p->i = 1
Run Code Online (Sandbox Code Playgroud)

但是,如果您更改它void* p;,A* p;它会给出预期的行为.为什么?

另一个问题,我无法逃脱,(A*&)否则我无法使用operator ++; 但它也会发出警告,因为取消引用类型惩罚指针会破坏严格别名规则.有没有可行的方法来克服警告?

Jam*_*lis 11

好吧,正如编译器警告你的那样,你违反了严格的别名规则,这正式意味着结果是未定义的.

您可以通过使用增量的函数模板来消除严格的别名冲突:

template<typename T>
void advance_pointer_as(void*& p, int n = 1) {
    T* p_a(static_cast<T*>(p));
    p_a += n;
    p = p_a;
}
Run Code Online (Sandbox Code Playgroud)

使用此函数模板,以下定义可main()生成Ideone编译器上的预期结果(并且不发出警告):

int main()
{
    void* p = new A[3];
    std::cout << "p->i = " << static_cast<A*>(p)->i << std::endl;
    advance_pointer_as<A>(p);
    std::cout << "p->i = " << static_cast<A*>(p)->i << std::endl;
    advance_pointer_as<A>(p);
    std::cout << "p->i = " << static_cast<A*>(p)->i << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

  • 一个缓慢,正确的程序远远优于快速,错误的程序. (7认同)
  • 尽管我的讽刺评论,我希望编译器即使在低优化级别也可以内联,所以我不希望它有显着的性能成本. (2认同)

AnT*_*AnT 6

您已经收到了正确的答案,这确实违反了严格的别名规则,导致代码无法预测的行为.我只是注意到你的问题的标题引用了"回送指向原始类的指针".实际上,您的代码与"返回"任何东西都没有任何关系.您的代码会将void *指针占用的原始内存内容重新解释A *指针.这不是"退回".这是重新解释.甚至远不是同一件事.

说明差异的一个好方法是使用intfloat示例.一个float声明和初始化值

float f = 2.0;
Run Code Online (Sandbox Code Playgroud)

cab被转换(显式或隐式转换)到int类型

int i = (int) f;
Run Code Online (Sandbox Code Playgroud)

与预期的结果

assert(i == 2);
Run Code Online (Sandbox Code Playgroud)

这确实是演员(转换).

或者,float也可以将相同的值重新解释为int

int i = (int &) f;
Run Code Online (Sandbox Code Playgroud)

然而,在这种情况下,价值i将是完全无意义的并且通常是不可预测的.我希望通过这些示例很容易看出转换和内存重新解释之间的区别.

重新解释正是您在代码中所做的.的(A *&) p表达不是别的,只是通过指针所占据原始存储器的重新解释void *p为类型的指针A *.该语言不保证这两种指针类型具有相同的表示形式,甚至具有相同的大小.因此,期望代码中的可预测行为就像期望上面的(int &) f表达式来评估2.

真正"反击" void *指针的正确方法是做(A *) p,而不是(A *&) p.结果(A *) p确实是原始指针值,可以通过指针算法安全地操作.获取原始值作为左值的唯一正确方法是使用附加变量

A *pa = (A *) p;
...
pa++;
...
Run Code Online (Sandbox Code Playgroud)

并且没有合法的方法来创建一个"就地"的左值,正如你的(A *&) p演员所尝试的那样.代码的行为就是一个例子.