0

使用 gcc/g++ 4.6.1 这是编译器错误还是语言功能?尽管编译器没有对我大喊大叫,但我想这至少是编译器的一个缺点。

我有一个运算符new重载的父类:

class C{ // just here to be faithful to the original code
  int y;
}

class A{
public:
  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
      = enfacia_size
      + item::size() * count; // the 'tail'
      ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  }
  uint count;
}

class B : public C, public A{
public:
    int i;
};

对象本身是可变长度的,所以它需要知道它有多长。因此有一个count领域。在此调用中,计数位于偏移量 0 处:

...
new A *pt = new(10) A;  // offset_of(A,count)==0
new B *pt = new(10) B;  // offset_of(B,count)==4

这是问题所在,在 operator 内部,无论是从父级调用还是从子级调用,它new的值总是写在偏移量 0 处。count那么,当用于继承时,程序会崩溃.. 静态方法和继承是否存在问题?这是怎么回事?

4

8 回答 8

3

您提出的解决方案通过在该类的生命周期之外访问非 POD 类的成员来调用未定义的行为。

阅读 C++2003 的§3.8,

T 类型对象的生命周期开始于:

  • 获得具有适合类型 T 的对齐和大小的存储,并且
  • 如果 T 是具有非平凡构造函数(12.1)的类类型,则构造函数调用已完成。

由于您的B对象有一个重要的构造函数,生命周期在B' 构造函数完成后开始。A的构造函数在 的构造函数完成之前运行B,因此在B.

在对象的生命周期开始之前,但在分配对象将占用的存储空间之后……任何指向对象将……所在的存储位置的指针都可以使用,但只能以有限的方式使用。... 如果对象 [满足某些条件,而你的],则程序在以下情况下具有未定义的行为:

  • 指针用于访问非静态数据成员或调用对象的非静态成员函数,或
  • 指针被隐式转换(4.10)为指向基类类型的指针,或者
  • 指针用作 static_cast (5.2.9) 的操作数(转换为 void* 或 void* 并随后转换为 char* 或 unsigned char* 时除外)。
  • 该指针用作 dynamic_cast (5.2.7) 的操作数。

因此,您提出的任何违反这四个条件之一的解决方案都会调用未定义行为。具体来说,您问题中的代码使用指针作为 a 的操作数static_cast,并使用它来访问非静态数据成员。


说了这么多,这里有一个程序,虽然没有由 C++ 标准定义,但对于您的特定编译器可能仍然有效。也就是说,这不是 C++ 程序,但它可能是 MSVC++ 程序或 G++ 程序:

#include <iostream>
#include <cstdlib>
class C{ // just here to be faithful to the original code
  int y;
};

template <class this_type>
class A {
public:
  void* operator new(size_t enfacia_size, unsigned int count){
      size_t total_size
      = enfacia_size
      + 42 * count; // the 'tail'
      ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  }
  unsigned int count;
};

class B : public C, public A<B>{
public:
    int i;
};

int main () {
  B *b = new(10) B;
  std::cout << b->count << "\n";
}
于 2012-05-01T19:20:06.723 回答
0

是的,多继承中的字段移动,这是一个显示移动字段的示例,其代码形式与原始示例相同。唯一显着的区别是,方法不是被称为“operator new”,而是称为“method”。在代码示例之后是显示字段移动的调试器输出,并且即使通过它移动了正确分配给该字段的值:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};


class A{
public:
  typedef A this_type;

  void* method(uint count){
    void *vp;
    vp = (void *)this;
    this_type *p;
    p = (this_type *)vp;
    p->count = count;   
  };

  uint count;
};

class B : public C, public A{
public:
    typedef B this_type;
    int i;
};

int main(){
  int j;

  A a;
  a.method(5);
  j++;

  B b;
  b.method(5);
  j++;

};

调试器输出:

(gdb) b main
Breakpoint 1 at 0x80484a1: file try_offsets_2.ex.cc, line 36.
(gdb) r
Starting program: /try_offsets_2 
Breakpoint 1, main () at try_offsets_2.ex.cc:36
(gdb) n
(gdb) 
$1 = (A *) 0xbffff758
(gdb) x /10x &a
0xbffff758: 0x00000005  0x003dbff4  0x08048500  0x003dbff4
0xbffff768: 0x00000000  0x0027d113  0x00000001  0xbffff804
0xbffff778: 0xbffff80c  0x0012eff4
(gdb) n
(gdb) x /10x &b
0xbffff74c: 0x003dc324  0x00000005  0x00296c55  0x00000005
0xbffff75c: 0x003dbff5  0x08048500  0x003dbff4  0x00000000
0xbffff76c: 0x0027d113  0x00000001
(gdb) 

请注意,在父项中位于偏移量 0 处的 5 在子项中位于偏移量 4 处。还要注意,尽管父方法被继承给子方法,但它的类型已更新,以便正确写入偏移量 4 处的字段。

于 2012-05-04T20:36:01.413 回答
0

这不可能奏效。operator new不知道正在分配的对象的类型。只有大小。由于您可以直接调用操作员,因此可能根本没有对象。

在每个派生类中定义this_type不同(我猜你在演示文稿中错过了这部分)不会影响它在基类方法中的含义。

于 2012-05-01T18:02:57.913 回答
0

您正在对“由于继承而移动的字段”提出一些奇怪的主张,但没有发生这样的事情。

试试这个程序:

#include <iostream>
#include <stdint.h>

struct C
{
    int i;
};

struct A
{
    int count;

    void f()
    {
        uintptr_t pt = reinterpret_cast<uintptr_t>(this);
        uintptr_t pc = reinterpret_cast<uintptr_t>(&count);
        std::cout << "Offset of A::count within A is " << (pc - pt) << '\n';
    }
};

struct B : C, A
{
    void g()
    {
        uintptr_t pt = reinterpret_cast<uintptr_t>(this);
        uintptr_t pc = reinterpret_cast<uintptr_t>(&count);
        std::cout << "Offset of A::count within B is " << (pc - pt) << '\n';
    }
};

int main()
{
    A a;
    a.f();
    B b;
    b.f();
    b.g();
}

这会产生:

Offset of A::count within A is 0
Offset of A::count within A is 0
Offset of A::count within B is 4

无论您在 B 类型的对象或 A 类型的对象上调用 A::f 都没有关系,它仍然是 A::f 并且它仍然以固定偏移量访问 A:count。当您调用 B::g 时该函数知道 A::count 成员与this

同样的事情发生在你的operator new,那个函数是 A 的成员并且对 B 一无所知。当你访问时,this_type::count你正在访问A::count。您将其称为 B 无关紧要,仅A::operator new存在(该函数并未B::operator new像您想象的那样被复制来生成)。

模板版本有效,因为类型this_type指的是 B 而不是 A,所以函数访问B::count

于 2012-05-04T12:54:35.893 回答
0

基类和派生类之间的偏移量不同是正常的,尤其是在涉及多重继承时。每当指针从一种类型转换为另一种类型时,编译器都会提供不可见的修正来修改指针地址。

它必须是这样的,因为每个指针类型在它指向的对象类型上必须是一致的。A* p1 = new A并且A* p2 = new B必须对 p1 和 p2 使用相同的偏移量。

于 2012-05-01T19:26:20.003 回答
0

这编译得很好:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};

class A{
public:
  typedef A this_type;

  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
    = enfacia_size
    + sizeof(int) * count; // the 'tail'
    ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  };
  uint count;
};

class B : public C, public A{
public:
    int i;
};

int main(){
  B *b_pt = new(5) B;  
  uint j=0;
  j++;
};

这是gdb中显示的“问题”:

(gdb) b main
Breakpoint 1 at 0x80484e1: file try_offsets.ex.cc, line 32.
(gdb) r
Starting program try_offsets 
Breakpoint 1, main () at try_offsets.cc:32
(gdb) n
(gdb) p &(b_pt->count)
$1 = (uint *) 0x804a00c
(gdb) x/10 b_pt
0x804a008:  5   0   0   0
0x804a018:  0   0   0   0
0x804a028:  0   135129
(gdb) p b_pt
$2 = (B *) 0x804a008
(gdb) 

请注意,count 位于 0x804a00c,但分配给 count 时写入 0x804a008。现在有了 Rob 指出的变体,其中通过模板将这种类型设置为子项:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};

template<typename this_type>
class A{
public:

  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
    = enfacia_size
    + sizeof(int) * count; // the 'tail'
    ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  };
  uint count;
};

class B : public C, public A<B>{
public:
    int i;
};

int main(){
  B *b_pt = new(5) B;  
  uint j=0;
  j++;
};

我们得到正确的行为:

(gdb) b main
Breakpoint 1 at 0x80484e1: file try_offsets.ex.cc, line 32.
(gdb) r
Starting program
Breakpoint 1, main () at try_offsets.cc:32
(gdb) n
(gdb) p &(b_pt->count)
$1 = (uint *) 0x804a00c
(gdb) x/10 b_pt
0x804a008:  0   5   0   0
0x804a018:  0   0   0   0
0x804a028:  0   135129
(gdb) 

不过有趣的是,当 this_type 设置为“A”时,使用此解决方案将无法编译:

class B : public C, public A<A>{
public:
    int i;
};

给出:

try_offsets.cc:26:31: error: type/value mismatch at argument 1 in template parameter list for ‘template<class this_type> class A’
try_offsets.cc:26:31: error:   expected a type, got ‘A’

我确定的解决方案:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};


class A{
public:
  typedef A this_type;

  A(uint count):count(count){;}

  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
    = enfacia_size
    + sizeof(int) * count; // the 'tail'
    ;
      this_type *new_pt = (this_type *)malloc(total_size);
      return new_pt;
  };
  uint count;
};

class B : public C, public A{
public:
    B(uint count):A(count){;}
    int i;
};

int main(){
  B *b_pt = new(5) B(5);  
  uint j=0;
  j++;
};

虽然这打破了旧的惯例。分配器在分配点上方写入分配的长度是正常的。也就是说,例如,delete/free 如何知道堆上的块有多长。此外,由于 operator delete 不能带参数,这就是我们获取信息的方式。

于 2012-05-03T07:15:57.177 回答
0

这是另一个解决方案:

#include <cstddef>
#include <stdlib.h>

typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};

class A{
public:
  uint count;
};

class B : public C, public A{
public:
    int i;
};

template<typename T>
struct A_allocation_helper : T
{
  void* operator new(size_t enfacia_size, uint count){
    size_t total_size
      = enfacia_size
      + sizeof(int) * count; // the 'tail'
      ;
    T *new_pt = (T *)malloc(total_size);
    new_pt->count = count;
    return new_pt;
  };

};


int main(){
  B *b_pt = new(5) A_allocation_helper<B>;
  return b_pt->count;
};

现在进行内存分配的代码知道正确的类型,而不是由只知道基数的类型完成。

于 2012-05-04T23:42:19.470 回答
0

此代码在转换为本地类型的方法中也有一个 void 指针:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};


class A{
public:
  typedef A this_type;

  void* method(uint count){
    void *vp;
    vp = (void *)this;
    this_type *p;
    p = (this_type *)vp;
    p->count = count;   
  };

  uint count;
};

class B : public C, public A{
public:
    typedef B this_type;
    int i;
};

int main(){
  int j;

  A a;
  a.method(5);
  j++;

  B b;
  b.method(5);
  j++;

};

它按预期工作:

(gdb) b main
Breakpoint 1 at 0x80484a1: file try_offsets_2.ex.cc, line 36.
(gdb) r
Starting program: /try_offsets_2 
Breakpoint 1, main () at try_offsets_2.ex.cc:36
(gdb) n
(gdb) 
$1 = (A *) 0xbffff758
(gdb) x /10x &a
0xbffff758:    0x00000005    0x003dbff4    0x08048500    0x003dbff4
0xbffff768:    0x00000000    0x0027d113    0x00000001    0xbffff804
0xbffff778:    0xbffff80c    0x0012eff4
(gdb) n
(gdb) x /10x &b
0xbffff74c:    0x003dc324    0x00000005    0x00296c55    0x00000005
0xbffff75c:    0x003dbff5    0x08048500    0x003dbff4    0x00000000
0xbffff76c:    0x0027d113    0x00000001
(gdb)

您可以看到孩子的计数已设置。是的,我知道这是如何工作的,当然这是预期的结果。原因是,在调用 method() 时正确设置了“this”指针。

问题,这里的原始问题是 operator new 不会为孩子产生预期的结果,而是像父类型一样行事。然而,其中带有赋值的操作符 new 是在没有来自编译器的任何噪音的情况下继承的。

我认为,所有这一切的真正答案是 operator new 与其他方法的不同之处在于它没有传递 this 指针的幻像第一个操作数。以及上面提到的 nm 怎么可能呢,因为该对象还不存在。尽管类型定义已经存在,但编译器在多重继承中将类型传递给子方法的方式是通过幻像第一个操作数 this 指针,该指针已设置为指向对象中定义该方法的类型的正确区域.

如果编译器以 this 指针的方式为我们提供了 this_type,那么 operator new 可以通过强制转换 void * 来访问它创建的分配中的字段。这在许多情况下都非常有用。

但留给我的底线问题是,为什么编译器没有为非法赋值给出错误,然后生成代码,为子级生成错误值,如原始代码所示。(乔恩,是的,我给孩子打电话了 new ,它分配了作为父母的计数,没有警告等。公平地说,在任何普通程序员的眼中,这是一个意想不到的结果)。

于 2012-05-05T00:38:27.740 回答