11

考虑以下一对相关结构的声明。后代类不添加任何成员变量,唯一的成员函数是一个构造函数,它除了将其所有参数转发给基类的构造函数之外什么都不做。

struct Base {
  Base(int a, char const* b):
    a(a), b(b)
  { }
  int a;
  char const* b;
};

struct Descendant: Base {
  Descendant(int a, char const* b):
    Base(a, b)
  { }
};

现在考虑使用这些类型的以下代码。函数foo期望接收一个数组Base。但是,main定义了一个数组Descendant并将其传递给foo

void foo(Base* array, unsigned len)
{
  /* <access contents of array> */
}

int main()
{
  Descendant main_array[] = {
    Descendant(0, "zero"),
    Descendant(1, "one")
  };
  foo(main_array, 2);
  return 0;
}

该程序的行为是否已定义?答案是否取决于 的主体foo,例如它是写入数组还是只读取数组?

如果sizeof(Derived)不等于,则根据上一个关于派生对象数组的基指针的sizeof(Base)问题的答案,行为未定义。但是,这个问题中的对象是否有可能具有不同的大小?

4

5 回答 5

1

该程序的行为是否已定义?答案是否取决于 foo 的主体,例如它是写入数组还是只读取数组?

我会冒险回答说程序定义明确(只要foo是)即使它是用另一种语言(例如C)编写的。

如果sizeof(Derived)不等于sizeof(Base),则根据上一个关于派生对象数组的基指针的问题的答案,行为是未定义的。但是,这个问题中的对象是否有可能具有不同的大小?

我不这么认为。根据我对标准 (*) §9.2 第 17 条的阅读

如果两个标准布局结构(第 9 条)类型具有相同数量的非静态数据成员并且相应的非静态数据成员(按声明顺序)具有布局兼容类型(3.9),则它们是布局兼容的。

§9 第 7 到 9 条详细说明了布局兼容性的要求:

7标准布局类是这样的类:

  • 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,

  • 没有虚函数 (10.3) 和虚基类 (10.1),

  • 对所有非静态数据成员具有相同的访问控制(第 11 条),

  • 没有非标准布局的基类,

  • 要么在派生最多的类中没有非静态数据成员,并且最多有一个具有非静态数据成员的基类,要么没有具有非静态数据成员的基类,并且

  • 没有与第一个非静态数据成员相同类型的基类。

8标准布局结构是使用class-key structclass-key class定义的标准布局类。标准布局联合是使用class-key 定义的标准布局类union

9 [注意:标准布局类对于与用其他编程语言编写的代码进行通信很有用。它们的布局在 9.2 中指定。——尾注]

特别注意最后一个子句(结合§3.9)——根据我的阅读,这保证只要你不添加太多“C++ 东西”(虚拟函数等,从而违反标准布局要求)你的结构/classes 将表现为添加了语法糖的 C 结构。

如果Base没有构造函数,会有人怀疑其合法性吗?我不这么认为,因为这种模式(从添加构造函数/辅助函数的 C 结构派生)是惯用的。

我对我错的可能性持开放态度,欢迎补充/更正。

(*) 我这里实际上是看 N3290,但实际标准应该足够接近。

于 2013-11-08T20:43:35.983 回答
0

向类中添加新数据Descendent将破坏Descendent[]Base[]. 为了让某个函数假装一个较大结构的数组是一个较小但兼容的结构的数组,必须准备一个新数组,其中多余的字节被切掉,在这种情况下,不可能定义系统。如果一些指针被切掉会发生什么?如果这些对象的状态应该作为调用过程的一部分而改变,而它们所引用的实际对象不是原始对象,会发生什么?

否则,如果没有发生切片并且 aBase*被ed,将Derived[]被添加到它的二进制值中,并且它将不再指向 a 。在这种情况下,显然也无法定义系统的行为。++sizeof(Base)Base*

知道这一点,使用这个成语是不安全的,即使标准和总统和上帝定义它是有效的。任何添加都会Descendent破坏您的代码。即使你添加了一个断言,也会有一些函数的合法性取决于Base[]Descendent[]. 维护您的代码的任何人都必须追查每种情况并提出适当的解决方法。围绕这个习惯用法考虑你的程序以避免这些问题可能不值得方便。

于 2013-11-09T00:35:45.170 回答
0

如果您声明一个指向 Base 的指针数组,那么代码将正确运行。作为奖励,新的 foo() 将可以安全地与未来具有新数据结构的 Base 子类一起使用。

void foo(Base **array, unsigned len)
{
    // Example code
    for(unsigned i = 0; i < len; ++i)
    {
        Base *x = array[i];
        std::cout << x->a << x->b;
    }
}

void do_something()
{
    Base *data[2];
    data[0] = new Base(1, "a");
    data[2] = new Descendent(2, "b");

    foo(data, 2);

    delete data[0];
    delete data[1];
}
于 2013-11-07T22:13:03.033 回答
0

谨防!虽然这在您的编译器中几乎可以肯定是正确的,但标准并不能保证这可以正常工作。

至少添加 if (sizeof(Derived) != sizeof(Base)) logAndAbort("Derived and Base 的大小不匹配"); 查看。

如果您想知道,这是安全的编译器是一对一的,其中大小不会改变。标准中留下了一些东西,允许派生类与基类不连续。在发生这种情况的所有情况下,大小都必须增长(出于显而易见的原因)。

于 2013-11-07T18:33:34.633 回答
-1

虽然这个特定示例在我熟悉的所有现代平台和编译器上都是安全的,但总的来说它并不安全,而且它是一个糟糕代码的示例。

  1. 它不能在 sizeof(Base) != sizeof(Descendant) 的编译器/平台上工作
  2. 这是不安全的,因为有一天您的项目中的某个人会将新的非静态成员添加到 Descendant 类或将 Base 类设为虚拟。

UPD。Base 和 Descendant 都是标准布局类型。因此,标准的要求是指向 Descendant 的指针可以正确地 reinterpret_cast 为指向 Base 的指针,这意味着不允许在结构前面进行填充。但是 C++ 标准对结构末尾的填充没有任何要求,因此它依赖于编译器。还有一个标准提案明确将此行为标记为未定义。 http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1504

于 2013-11-08T22:08:51.553 回答