【问题标题】:Is this C++ structure initialization trick safe?这个 C++ 结构初始化技巧安全吗?
【发布时间】:2010-09-11 20:14:01
【问题描述】:

不必记住初始化一个简单的“C”结构,我可以从它派生并在构造函数中将其归零,如下所示:

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct()
    {
        memset(this, 0, sizeof(MY_STRUCT));
    }
};

这个技巧通常用于初始化 Win32 结构,有时可以设置无处不在的 cbSize 成员。

现在,只要没有用于 memset 调用销毁的虚函数表,这是一种安全的做法吗?

【问题讨论】:

  • 这个“技巧”通常被不知道如何使用 C++ 初始化功能的人使用。 C++ 已经有一个功能可以完全满足您的需求,但没有任何“技巧”。有关详细信息,请参阅 litb 的回复,但以防万一:将 MYSTRUCT()(是的,带有空大括号)放入 CMyStructs 构造函数初始化程序列表中将对基类子对象进行零初始化,而无需任何 memset hacks。
  • 我同意 AndreyT 的回答。您应该将 litb 的答案视为您的问题的正确答案(是的,这意味着不考虑我的问题,但是嘿,这是游戏!)... ^_^ ...

标签: c++ data-structures


【解决方案1】:

在 C++ 中不能保证类或结构的精确布局,这就是为什么您不应该从外部对其大小做出假设(这意味着如果您不是编译器)。

可能它可以工作,直到你找到一个它不支持的编译器,或者你将一些 vtable 加入其中。

【讨论】:

    【解决方案2】:

    序言:

    虽然我的回答还不错,但我发现 litb's answer 比我的要好,因为:

    1. 它教会了我一个我不知道的技巧(litb的答案通常有这种效果,但这是我第一次写下来)
    2. 它准确地回答了这个问题(即将原始结构的部分初始化为零)

    所以请在我之前考虑一下 litb 的回答。事实上,我建议问题的作者将 litb 的答案视为正确的答案。

    原答案

    在里面放一个真正的对象(即std::string)等会破坏,因为真正的对象会在memset之前初始化,然后被零覆盖。

    使用初始化列表不适用于 g++(我很惊讶...)。而是在 CMyStruct 构造函数主体中对其进行初始化。它将是 C++ 友好的:

    class CMyStruct : public MY_STRUCT
    {
    public:
        CMyStruct() { n1 = 0 ; n2 = 0 ; }
    };
    

    P.S.:当然,我假设你确实没有控制 MY_STRUCT。通过控制,您将直接在 MY_STRUCT 中添加构造函数而忘记了继承。请注意,您可以将非虚拟方法添加到类似 C 的结构中,并且仍然让它像结构一样运行。

    编辑:在 Lou Franco 的评论之后添加了缺失的括号。谢谢!

    编辑 2:我在 g++ 上尝试了代码,但由于某种原因,使用初始化列表不起作用。我使用主体构造函数更正了代码。不过,该解决方案仍然有效。

    请重新评估我的帖子,因为原始代码已更改(有关详细信息,请参阅更改日志)。

    编辑 3:阅读 Rob 的评论后,我想他有一点值得讨论:“同意,但这可能是一个巨大的 Win32 结构,可能会随着新的 SDK 而改变,所以 memset 是未来的证明。”

    我不同意:了解 Microsoft,它不会因为他们需要完美的向后兼容性而改变。他们将创建一个扩展的 MY_STRUCTEx 结构,其初始布局与 MY_STRUCT 相同,末尾有附加成员,并且可以通过“大小”成员变量(如用于 RegisterWindow、IIRC 的结构)来识别。

    因此,Rob 的评论中剩下的唯一有效点是“巨大的”结构。在这种情况下,memset 可能更方便,但您必须使 MY_STRUCT 成为 CMyStruct 的变量成员,而不是从它继承。

    我看到另一个 hack,但我想这会因为可能的结构对齐问题而中断。

    编辑 4:请查看 Frank Krueger 的解决方案。我不能保证它是可移植的(我猜是),但从技术角度来看它仍然很有趣,因为它展示了一种情况,在 C++ 中,“this”指针“地址”从其基类移动到其继承类.

    【讨论】:

    • 需要关闭 n1 的括号 -- 我同意这一点,顺便说一句
    • 初始化列表不起作用(对于派生的 CMyStruct),因为 n1 和 n2 不是它的直接成员。因此它们在 CMyStruct 构造函数被调用时已经被构造,所以你不能再次调用它们的构造函数。
    • 不同意这个作为公认的答案。初始化是编译器特定的。 Frank Krueger 和 Drealmer 的答案更好。
    • 你可以只做CMyStruct():MY_STRUCT() { },所有这些整数都将为零。
    • @paercebal:嗯,这意味着编译器损坏,这几乎不是反对() 形式的论据。 默认初始化和根本不初始化之间存在巨大差异,即使对于基本类型也是如此。对于像 OP 中的结构这样的结构,() 初始化程序会将所有内容设置为零。
    【解决方案3】:

    如果你已经有一个构造函数,为什么不直接用 n1=0 初始化它呢? n2=0; -- 这当然是更正常的方式。

    编辑:实际上,正如 paercebal 所示,ctor 初始化更好。

    【讨论】:

    • 同意,但这可能是一个巨大的 Win32 结构,可能会随着新的 SDK 而改变,所以 memset 是未来的证明。
    • Lou Franco:我确实错过了一些东西,因为 g++ 的测试显示初始化列表不允许我初始化来自 MY_STRUCT 的数据。我会做一些研究。 +1。
    • 罗布,你就在那儿。不过,如果您在 CMyStruct 中放置一些真正的对象,您使用 memset(this) 的解决方案将会中断。不过,我想还有一个替代方案......
    【解决方案4】:

    这会让我感觉更安全,因为即使有 vtable(或者编译器会尖叫),它也应该可以工作。

    memset(static_cast<MY_STRUCT*>(this), 0, sizeof(MY_STRUCT));
    

    我确信您的解决方案会奏效,但我怀疑在混合 memset 和类时是否能做出任何保证。

    【讨论】:

    • 我想知道:如果 CMyStruct 有一个 vtable,而 MY_STRUCT 是一个 C POD,那么它们将不会有相同的“this”地址。因此,投射 this 会将地址移动到正确的位置。所以我猜弗兰克克鲁格的解决方案可能是正确的,但我需要大师的确认。 +1。
    • P.S.:我在 g++ 上对其进行了测试,并且它有效(MY_STRUCT 的“this”指针在 CMyStruct 之后 4 个字节)。但问题仍然存在:这是一个 hack,还是真正的可移植 C++。我猜是后者。
    • paercebal - 我认为演员阵容会做得很好。我的意思是,我们确实保证了结构的数据连续性及其大小,所以我不明白为什么这会失败。
    • 下面的代码崩溃了:(。MSVC 2005 struct MY_STRUCT2 { int n1; int n2; virtual void a()=0; }; struct MY_STRUCT : MY_STRUCT2 { void a() { printf("% d ", sizeof(*this)); }; }; { MY_STRUCT s; memset(static_cast(&s), 0, sizeof(MY_STRUCT)); static_cast(&s)->a() ;
    • @acidzombie24:你的代码当然会崩溃:基础结构 MY_STRUCT2 有一个虚拟表!当前的问题是关于 POD 基础结构,而不是启用虚拟表的基础结构! ... ^_^ ...
    【解决方案5】:

    我的意见是不。我也不确定它会得到什么。

    随着您对 CMyStruct 的定义发生更改并添加/删除成员,这可能会导致错误。很容易。

    为 CMyStruct 创建一个构造函数,它接受一个 MyStruct 有一个参数。

    CMyStruct::CMyStruct(MyStruct &)
    

    或者类似的东西。然后,您可以初始化公共或私有“MyStruct”成员。

    【讨论】:

    • 我同意。这是另一个可行的解决方案。 +1。
    【解决方案6】:

    比 memset 好多了,你可以用这个小技巧来代替:

    MY_STRUCT foo = { 0 };
    

    这会将所有成员初始化为 0(或其默认值 iirc),无需为每个成员指定值。

    【讨论】:

    • 这是一个次优的解决方案,因为您必须在代码中的任何地方初始化结构,而构造函数将被编写一次且仅一次。
    • 您可以使用这种技术,但封装在一个类中 - 请参阅下面的答案。
    • 首先,这就是您在 C 中的操作方式。在 C++ 中,这个特定的小技巧转换为 MY_STRUCT foo = {};(因为并非所有数据类型都可以在 C++ 中使用文字 0 进行初始化)。其次,当我们谈论构造函数初始化列表时,直接的类似就是在初始化列表中包含MY_STRUCT()。
    • 这个技巧的问题是它污染了 GCC 中的-Wmissing-field-initializers,这是一个有用的警告。
    • 作为记录,这种技术似乎不适用于类字段,当然它不能在任意位置将所有成员重置为零,因为它必须在变量所在的地方使用声明。
    【解决方案7】:

    这是将 C 习语移植到 C++ 的完美示例(以及为什么它可能并不总是有效......)

    使用 memset 时会遇到的问题是,在 C++ 中,结构和类完全一样,只是默认情况下,结构具有公共可见性,而类具有私有可见性。

    因此,如果稍后,一些好心的程序员像这样更改 MY_STRUCT:

    
    struct MY_STRUCT
    {
        int n1;
        int n2;
    
       // Provide a default implementation...
       virtual int add() {return n1 + n2;}  
    };
    

    通过添加单个函数,您的 memset 现在可能会造成严重破坏。 comp.lang.c+有详细讨论

    【讨论】:

    • 然后我解雇了说程序员更改系统头文件。
    • 没关系。原始代码只是 U.B.因为无法保证 MY_STRUCT 子对象位于派生类的开头。
    【解决方案8】:

    从 ISO C++ 的角度来看,有两个问题:

    (1) 对象是 POD 吗?首字母缩略词代表普通旧数据,标准列举了您在 POD 中不能拥有的内容(维基百科有一个很好的总结)。如果不是 POD,则不能 memset。

    (2) 是否存在所有位为零无效的成员?在 Windows 和 Unix 上,NULL 指针全为零;它不需要。浮点 0 在很常见的 IEEE754 和 x86 中所有位都为零。

    Frank Kruegers 提示通过将 memset 限制为非 POD 类的 POD 基础来解决您的问题。

    【讨论】:

      【解决方案9】:

      试试这个 - 重载新的。

      编辑:我应该添加 - 这是安全的,因为在调用 any 构造函数之前内存已清零。大缺陷 - 只有在动态分配对象时才有效。

      struct MY_STRUCT
      {
          int n1;
          int n2;
      };
      
      class CMyStruct : public MY_STRUCT
      {
      public:
          CMyStruct()
          {
              // whatever
          }
          void* new(size_t size)
          {
              // dangerous
              return memset(malloc(size),0,size);
              // better
              if (void *p = malloc(size))
              {
                  return (memset(p, 0, size));
              }
              else
              {
                  throw bad_alloc();
              }
          }
          void delete(void *p, size_t size)
          {
              free(p);
          }
      
      };
      

      【讨论】:

      • 如果您先使用malloc(),然后是memset(,0,),那么您可能首先需要calloc()(操作系统可以,而且有些确实可以显着优化后者而不是前者)。无论如何,这似乎比使用基类初始化器没有任何优势。
      【解决方案10】:

      这些示例具有“未指定的行为”。

      对于非 POD,编译器布局对象(所有基类和成员)的顺序是未指定的 (ISO C++ 10/3)。考虑以下几点:

      struct A {
        int i;
      };
      
      class B : public A {       // 'B' is not a POD
      public:
        B ();
      
      private:
        int j;
      };
      

      这可以这样排列:

      [ int i ][ int j ]
      

      或如:

      [ int j ][ int i ]
      

      因此,直接在 'this' 的地址上使用 memset 是非常不确定的行为。上面的一个答案,乍一看似乎更安全:

       memset(static_cast<MY_STRUCT*>(this), 0, sizeof(MY_STRUCT));
      

      但是,我相信,严格来说,这也会导致未指定的行为。我找不到规范文本,但是 10/5 中的注释说:“基类子对象的布局(3.7)可能与相同类型的最派生对象的布局不同”。

      因此,我的编译器可以对不同的成员进行空间优化:

      struct A {
        char c1;
      };
      
      struct B {
        char c2;
        char c3;
        char c4;
        int i;
      };
      
      class C : public A, public B
      {
      public:
        C () 
        :  c1 (10);
        {
          memset(static_cast<B*>(this), 0, sizeof(B));      
        }
      };
      

      可以这样布局:

      [ char c1 ] [ char c2, char c3, char c4, int i ]
      

      在 32 位系统上,由于“B”的对齐等原因,sizeof(B) 很可能是 8 个字节。但是,如果编译器打包数据成员,sizeof(C) 也可以是“8”字节。因此,对 memset 的调用可能会覆盖给 'c1' 的值。

      【讨论】:

      • 我不明白为什么 this 到 MY_STRUCT* 的演员表应该更安全。当只涉及单继承时,它不会导致相同的地址吗?我不懂这个技巧
      • 强制转换是必要的,因为您不能保证“B”从“C”的起始地址开始。因此,强制转换偏移了“this”指针,使其指向“B”的开头。否则,我们可能会首先将“A::C1”设置为零,直到“B::i”的第 3 个字节。
      • 哦,我在您的第一个示例中没有发现,您只想清除 i。我同意如果是这种情况,那么在我看来,在单继承场景中也需要强制转换。
      【解决方案11】:

      这是一些代码,但它是可重用的;包含一次,它应该适用于任何 POD。您可以将此类的实例传递给任何需要 MY_STRUCT 的函数,或使用 GetPointer 函数将其传递给将修改结构的函数。

      template <typename STR>
      class CStructWrapper
      {
      private:
          STR MyStruct;
      
      public:
          CStructWrapper() { STR temp = {}; MyStruct = temp;}
          CStructWrapper(const STR &myStruct) : MyStruct(myStruct) {}
      
          operator STR &() { return MyStruct; }
          operator const STR &() const { return MyStruct; }
      
          STR *GetPointer() { return &MyStruct; }
      };
      
      CStructWrapper<MY_STRUCT> myStruct;
      CStructWrapper<ANOTHER_STRUCT> anotherStruct;
      

      这样,您不必担心 NULL 是否全为 0 或浮点表示。只要 STR 是一个简单的聚合类型,一切都会奏效。当 STR 不是简单的聚合类型时,您会收到编译时错误,因此您不必担心意外误用它。另外,如果类型包含更复杂的东西,只要它有一个默认构造函数,就可以了:

      struct MY_STRUCT2
      {
          int n1;
          std::string s1;
      };
      
      CStructWrapper<MY_STRUCT2> myStruct2; // n1 is set to 0, s1 is set to "";
      

      不利的一面是,由于您要制作额外的临时副本,因此速度较慢,并且编译器会将每个成员单独分配给 0,而不是一个 memset。

      【讨论】:

      • 首先,这个实现包含一个完全不必要的怪癖/错误/错误/任何东西。如果STR 的第一个成员不接受0 初始化器(例如,它具有枚举类型),则代码将无法编译。请注意,这是 C++,而不是 C,默认聚合初始化的正确习惯用法是 STR temp = {},而不是上面的 STR temp = { 0 }。
      • 其次,仍然很难证明所有这些努力的合理性,因为 C++ 已经为构造函数初始化列表中可用的{} 聚合语法提供了简单明了的直接类似物。它是 () 初始化程序。我们有一个帖子有十多个回复,几乎所有回复都在尝试解决一个不存在的问题:)
      【解决方案12】:

      我所做的是使用聚合初始化,但只为我关心的成员指定初始化器,例如:

      STARTUPINFO si = {
          sizeof si,      /*cb*/
          0,              /*lpReserved*/
          0,              /*lpDesktop*/
          "my window"     /*lpTitle*/
      };
      

      其余成员将被初始化为适当类型的零(如 Drealmer 的帖子中所示)。在这里,您相信 Microsoft 不会通过在中间添加新的结构成员来无故破坏兼容性(一个合理的假设)。这个解决方案给我的印象是最优的——一个语句,没有类,没有 memset,没有关于浮点零或空指针的内部表示的假设。

      我认为涉及继承的技巧是可怕的风格。对于大多数读者来说,公共继承意味着 IS-A。另请注意,您从一个并非设计为基础的类继承。由于没有虚拟析构函数,通过指向基类的指针删除派生类实例的客户端将调用未定义的行为。

      【讨论】:

      • 从 OO 设计的角度来看,这是一个 hack,你是对的。但从 C++ 的角度来看,它不是:只有非虚方法的类只是一个附加了函数的结构。而且没有额外的成员变量,无论如何都不需要虚拟析构函数......
      • 你说得对。在我看到的大多数实现中,它导致派生的析构函数没有被调用。但同样,你是对的,不应该玩多态性(即通过其基指针删除它)以避免未定义的行为。
      • 公共继承仅在 OOP 范式中意味着“IS-A”。 C++ 是一种多范式语言。一般来说,C++ 公共继承在 OOP 范式之外有其用途,例如在模板元编程中。一个简单的例子是从std::iterator 继承std::back_insert_iterator 的公共继承。它并不以任何方式暗示 OOP 的 IS-A。
      【解决方案13】:

      如果 MY_STRUCT 是您的代码,并且您乐于使用 C++ 编译器,则可以将构造函数放在那里而不用包装在类中:

      struct MY_STRUCT
      {
        int n1;
        int n2;
        MY_STRUCT(): n1(0), n2(0) {}
      };
      

      我不确定效率,但我讨厌在你没有证明效率是必要的时候耍花招。

      【讨论】:

      • 代码清晰简洁,尤其是考虑到后续维护始终是正确的方法。
      • @jwhitlock:在原来的 POD 结构中添加构造函数是一个显着的质变。做出这种改变的决定应该在设计层面做出。即使MY_STRUCT 在我们的控制范围之内,仅仅出于某种极端局部的原因,进行如此剧烈的更改,例如剥夺MY_STRUCT 的 POD 属性也是一个设计错误。
      【解决方案14】:

      您可以简单地对基进行值初始化,它的所有成员都将被清零。这是有保证的

      struct MY_STRUCT
      {
          int n1;
          int n2;
      };
      
      class CMyStruct : public MY_STRUCT
      {
      public:
          CMyStruct():MY_STRUCT() { }
      };
      

      为此,基类中不应有用户声明的构造函数,就像在您的示例中一样。

      没有讨厌的memset。不能保证 memset 在您的代码中有效,即使它应该在实践中有效。

      【讨论】:

      • 这是最相关和正确的答案。 (我想知道是否可以在 SO 上做一些事情,以便将平衡从“最早”的答案转移到“最正确”的答案。不幸的是,目前较早的答案大部分时间都击败了正确的答案。)跨度>
      • @litb :抱歉,我误解了您对我帖子的原始评论,并且需要 AndreyT 自己的评论才能使其正确。在我看来,你的答案是正确的,我修改了我的帖子以澄清这一点。当然,+1 给你的答案。谢谢!... ^_^ ...
      【解决方案15】:

      我假设该结构已提供给您并且无法修改。如果可以更改结构,那么显而易见的解决方案是添加构造函数。

      当您只需要一个简单的宏来初始化您的结构时,不要使用 C++ 包装器过度设计您的代码。

      #include <stdio.h>
      
      #define MY_STRUCT(x) MY_STRUCT x = {0}
      
      struct MY_STRUCT
      {
          int n1;
          int n2;
      };
      
      int main(int argc, char *argv[])
      {
          MY_STRUCT(s);
      
          printf("n1(%d),n2(%d)\n", s.n1, s.n2);
      
          return 0;
      }
      

      【讨论】:

        【解决方案16】:

        评论litb's answer(好像还不能直接评论):

        即使使用这种不错的 C++ 风格的解决方案,您也必须非常小心,不要将它天真地应用于包含非 POD 成员的结构。

        一些编译器不再正确初始化。

        见this answer to a similar question。 我个人在 VC2008 上的体验很糟糕,额外添加了 std::string。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-12-07
          • 1970-01-01
          • 2011-02-01
          • 1970-01-01
          • 2012-07-16
          相关资源
          最近更新 更多