web-dev-qa-db-ja.com

このC ++構造の初期化トリックは安全ですか?

単純な 'C'構造を初期化することを覚えておく必要はなく、それから派生させて、次のようにコンストラクターでゼロにすることができます。

struct MY_STRUCT
{
    int n1;
    int n2;
};

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

このトリックは、Win32構造の初期化によく使用され、ユビキタスなcbSizeメンバーを設定できる場合があります。

さて、破壊するmemset呼び出し用の仮想関数テーブルがない限り、これは安全な方法ですか?

31
Rob

前文:

私の答えはまだ大丈夫ですが、 litbの答え は私のものよりもはるかに優れています:

  1. それは私が知らなかったトリックを教えてくれます(litbの回答には通常この効果がありますが、これを書き留めるのはこれが初めてです)
  2. それは質問に正確に答えます(つまり、元の構造体の部分をゼロに初期化します)

それで、私の前にlitbの答えを考えてください。実際、私はlitbの答えを正しいものと考えるように質問の作者に提案します。

元の答え

真のオブジェクト(つまりstd :: string)などを中に入れると、本当のオブジェクトがmemsetの前に初期化され、次にゼロで上書きされるため、壊れます。

初期化リストを使用しても、g ++では機能しません(私は驚いています...)。代わりに、CMyStructコンストラクター本体で初期化します。 C++フレンドリーになります:

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

PS:もちろん、MY_STRUCTをnoで制御していたと思います。制御を使用すると、コンストラクターをMY_STRUCT内に直接追加し、継承を忘れてしまいます。 Cのような構造体に非仮想メソッドを追加しても、構造体として動作することに注意してください。

編集:ルーフランコのコメントの後に、欠落していた括弧を追加しました。ありがとう!

編集2:g ++でコードを試しましたが、何らかの理由で、初期化リストを使用しても機能しません。 bodyコンストラクターを使用してコードを修正しました。ただし、解決策はまだ有効です。

元のコードが変更されたため、投稿を再評価してください(詳細については、変更ログを参照してください)。

編集3:Robのコメントを読んだ後、彼は議論に値するポイントを持っていると思います。

同意しない:マイクロソフトを知っていて、完全な下位互換性が必要なため変更されない。代わりに、拡張MY_STRUCTを作成しますEx MY_STRUCTと同じ初期レイアウトの構造体。末尾に追加のメンバーがあり、RegisterWindow、IIRCで使用される構造体のような「サイズ」メンバー変数を通じて認識できます。

したがって、Robのコメントから残っている唯一の有効な点は、「巨大な」構造体です。この場合、おそらくmemsetがより便利ですが、MY_STRUCTをCMyStructから継承する代わりに、CMyStructの変数メンバーにする必要があります。

別のハックを見ましたが、構造体の整列問題の可能性があるため、これは壊れると思います。

編集4:フランク・クルーガーのソリューションを見てください。移植可能かどうかは保証できませんが(おそらくそうです)、C++では「this」ポインタの「アドレス」が基本クラスから継承されたクラスに移動する1つのケースを示しているため、技術的な観点からは興味深いものです。 。

19
paercebal

単純にベースの値を初期化することができ、そのすべてのメンバーはゼロで初期化されます。これは保証されています

struct MY_STRUCT
{
    int n1;
    int n2;
};

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

これが機能するためには、例のように、基本クラスにユーザー宣言のコンストラクターがあってはなりません。

嫌なmemsetはありません。実際に機能するはずですが、memsetがコードで機能することは保証されていません。

Memsetよりもはるかに優れていますが、代わりにこの小さなトリックを使用できます。

MY_STRUCT foo = { 0 };

これにより、すべてのメンバーが0(またはデフォルト値iirc)に初期化されます。それぞれに値を指定する必要はありません。

12
Drealmer

これは、vtableがある場合でも機能するので(またはコンパイラが悲鳴を上げる)、私ははるかに安全だと感じます。

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

私はあなたの解決策がうまくいくと確信していますが、memsetとクラスを混合するときに行われる保証はありません。

9
Frank Krueger

これは、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 + に詳細な説明があります

4
Benoit

例には「不特定の動作」があります。

非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を直接使用することは、ほとんど規定されていない動作です。上記の答えの1つは、一見すると安全なように見えます。

 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」に指定された値が上書きされる可能性があります。

3
Richard Corden

クラスまたは構造の正確なレイアウトはC++では保証されていません。そのため、外部から(つまり、コンパイラーでない場合は)サイズまたはサイズを想定しないでください。

おそらく、動作しないコンパイラが見つかるか、vtableをミックスに投入するまでは機能します。

2
Florian Bösch

litbの回答 へのコメント(まだ直接コメントすることは許可されていないようです):

この素敵なC++スタイルのソリューションを使用する場合でも、POD以外のメンバーを含む構造体にこれを単純に適用しないように注意する必要があります。

その後、一部のコンパイラは正しく正しく初期化されなくなります。

同様の質問に対するこの回答 を参照してください。私は個人的にVC2008でstd :: stringを追加することで悪い経験をしました。

1
yau

MY_STRUCTがコードで、C++コンパイラを使用して満足している場合は、クラスをラップせずにコンストラクタをそこに配置できます。

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

効率についてはわかりませんが、効率が必要であると証明されていないときにトリックを行うのは嫌いです。

1
jwhitlock

ISO C++の観点から、2つの問題があります。

(1)オブジェクトはPODですか?頭字語はPlain Old Dataを表しており、標準はPODに含めることができないものを列挙しています(ウィキペディアには良い要約があります)。 PODでない場合は、memsetできません。

(2)all-bits-zeroが無効なメンバーはありますか? WindowsおよびUnixでは、NULLポインターはすべてビット0です。必要はありません。浮動小数点0は、IEEE754ですべてのビットがゼロになっています。これは一般的ですが、x86でも同じです。

Frank Kruegersのヒントは、memsetを非PODクラスのPODベースに制限することで、あなたの懸念に対処します。

1
MSalters

これを試してください-新しいオーバーロード。

編集:追加する必要があります-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);
    }

};
1
Thanatopsis

すでにコンストラクタがある場合は、n1 = 0でコンストラクタを初期化しないでください。 n2 = 0; -それは確かにもっとnormalの方法です。

編集:実際、paercebalが示したように、ctorの初期化はさらに優れています。

1
Lou Franco

私の意見はノーです。何が得られるのかよくわかりません。

CMyStructの定義が変更され、メンバーを追加/削除すると、バグが発生する可能性があります。簡単に。

MyStructがパラメーターを持つCMyStructのコンストラクターを作成します。

CMyStruct::CMyStruct(MyStruct &)

または求めていたもの。その後、パブリックまたはプライベートの「MyStruct」メンバーを初期化できます。

1
kervin

私が行うことは、集約初期化を使用することですが、気になるメンバーの初期化子のみを指定します。

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

残りのメンバーは、適切なタイプのゼロに初期化されます(Drealmerの投稿と同様)。ここでは、途中で新しい構造体のメンバーを追加することで、Microsoftが不必要に互換性を壊さないようにしています(妥当な仮定)。この解決策は私に最適だと思います-1つのステートメント、クラス、memset、浮動小数点ゼロまたはnullポインターの内部表現に関する仮定はありません。

継承を伴うハックは恐ろしいスタイルだと思います。パブリック継承とは、ほとんどの読者にとってIS-Aを意味します。また、ベースとして設計されていないクラスから継承していることにも注意してください。仮想デストラクタがないため、baseへのポインタを通じて派生クラスインスタンスを削除するクライアントは、未定義の動作を呼び出します。

0
fizzer

少しコードですが、再利用可能です。一度含めると、どの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 "";

マイナス面としては、追加の一時コピーを作成するため速度が遅くなり、コンパイラーは各メンバーを1つのmemsetではなく個別に0に割り当てます。

0
Eclipse

構造はあなたに提供されており、変更できないと思います。構造を変更できる場合、明らかな解決策はコンストラクタを追加することです。

構造を初期化する単純なマクロだけが必要な場合は、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;
}
0
grob