这个C ++结构初始化技巧安全吗?

时间:2020-03-06 14:30:54  来源:igfitidea点击:

不必记住初始化一个简单的" 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 ++中,不能保证类或者结构的精确布局,这就是为什么我们不应该从外部对类或者结构的大小进行假设(也就是说,如果我们不是编译器)。

可能会奏效,直到找到不支持它的编译器,或者将一些vtable放入混合中。

前言:

虽然我的答案仍然可以,但我发现litb的答案比我的要好得多,因为:

  • 它教会了我一个我不知道的窍门(litb的答案通常会产生这种效果,但这是我第一次写下来)
  • 它精确地回答了这个问题(即,将原始结构的部分初始化为零)

因此,请在我之前先考虑一下litb的答案。实际上,我建议问题的作者将litb的答案视为正确的答案。

原始答案

在其中放入一个真实对象(即std :: string)等将中断,因为该真实对象将在内存集之前初始化,然后被零覆盖。

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

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

附注:我以为我们当然无法控制MY_STRUCT。通过控制,我们将直接在MY_STRUCT内部添加了构造函数,而忘记了继承。请注意,我们可以将非虚拟方法添加到类似C的结构中,并且仍然使其表现为结构。

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

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

由于原始代码已更改,请重新评估我的帖子(有关更多信息,请参见changelog)。

编辑3:阅读Rob的评论后,我想他有一个值得讨论的观点:"同意,但这可能是巨大的Win32结构,可能会随着新的SDK发生变化,因此memset可以作为未来的证明。"

我不同意:了解微软,它不会改变,因为他们需要完美的向后兼容性。他们将改为创建扩展的MY_STRUCTEx结构,该结构的初始布局与MY_STRUCT相同,并在末尾添加成员,并可以通过" size"成员变量来识别,例如用于RegisterWindow IIRC的结构。

因此,罗伯评论中唯一剩下的有效点就是"巨大的"结构。在这种情况下,也许memset更方便,但是我们必须使MY_STRUCT成为CMyStruct的变量成员,而不是从其继承。

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

编辑4:请看一下弗兰克·克鲁格(Frank Krueger)的解决方案。我不能保证它是可移植的(我想是的),但是从技术角度来看,它仍然很有趣,因为它显示了一种情况,在C ++中," this"指针" address"从其基类移至其继承的类。 。

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

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

这会让我感到更加安全,因为即使有一个" vtable",它也应该可以工作(否则编译器会尖叫)。

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

我敢肯定解决方案会起作用,但是我怀疑在混合memset和类时是否有任何保证。

我的看法是不。我也不知道它会得到什么。

随着我们对CMyStruct的定义的更改以及我们添加/删除成员,这可能会导致错误。容易地。

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

CMyStruct::CMyStruct(MyStruct &)

或者类似的东西。然后,我们可以初始化公共或者私有" MyStruct"成员。

比记忆集要好得多,我们可以改用以下小技巧:

MY_STRUCT foo = { 0 };

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

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

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

因此,如果以后有什么好主意的程序员像这样更改MY_STRUCT,该怎么办:

struct MY_STRUCT
{
    int n1;
    int n2;

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

通过添加该单个函数,内存集现在可能会造成破坏。
在comp.lang.c +中有详细的讨论

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

(1)对象是POD吗?该首字母缩写词代表"原始数据",该标准枚举了POD中没有的内容(维基百科提供了很好的摘要)。如果不是POD,则无法进行设置。

(2)是否存在全零位无效的成员?在Windows和Unix上,NULL指针均为零。不必如此。在x86上,这很常见,在IEEE754中,浮点0的所有位均为零。

Frank Kruegers技巧通过将内存集限制为非POD类的POD库来解决问题。

试试这个新的重载。

编辑:我应该添加这是安全的,因为在调用任何构造函数之前内存已归零。仅当动态分配对象时,大缺陷才起作用。

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);
    }

};

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

对于非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'的值。

这是一些代码,但是可以重用;包括一次,它应该适用于任何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,而不是一个内存集。

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

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

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

我认为涉及继承的技巧很糟糕。对于大多数读者来说,公共继承意味着IS-A。还要注意,我们是从一个不是设计为基础的类继承的。由于没有虚拟析构函数,因此通过指向基址的指针删除派生类实例的客户端将调用未定义的行为。

如果MY_STRUCT是代码,并且对使用C ++编译器感到满意,则可以将构造函数放在那里,而无需将其包装在类中:

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

我不确定效率,但是当我们没有证明需要效率时,我讨厌花招。

我们可以简单地对基值进行初始化,其所有成员将被清零。这样可以保证

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct():MY_STRUCT() { }
};

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

没有讨厌的memset。即使可以在实践中使用,也不能保证memset在代码中有效。

我认为结构是提供给,无法修改。如果可以更改结构,则显而易见的解决方案是添加构造函数。

当我们想要的只是一个简单的宏来初始化结构时,请不要使用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;
}