none
#pragma pack を使うと変数が壊れてしまう

    質問

  • C++でいろいろとやっていたら
    #pragma packを使うと変数を壊しているようなのですが
    その理由がわからないので教えてほしいです。

    ソリューションの構造体アライメントは標準にしてあります。
    Class2.hに #pragma pack(push,2) と記述すると
    Class2.cpp で m_Class2_Arrow.Class1_Init();の関数に入って
    m_Class1_dWidth = 8.0;とやっても値が設定されず
    関数を抜けるとm_Class2_Str1の変数が壊れてしまいます。

    壊れると言っているのはデバッグのウォッチで見ると
    以下の状態になってしまうということです。

    m_Class2_Str1 <文字列の文字の読み取り中にエラーが発生しました。> ATL::CStringT<char,ATL::StrTraitATL<char,ATL::ChTraitsCRT<char> > >

    その他試したことは・・・

    1.Class2.hの #pragma pack(push,2)をコメントアウトすると発生しない
      ソリューション側で構造体アライメントは規定になってます

    2.Class2.hの #pragma pack(push,2)をコメントアウトして
      ソリューション側で構造体アライメントを2(/Zp2)にしても発生しない
    3.Class2.hの #pragma pack(push,2)をコメントアウトして
      ソリューション側で構造体アライメントを8(/Zp2)にしても発生しない
    4.Class2.hの #pragma pack(push,2)を有効にしたまま
      ソリューションにて構造体アライメントを2(/Zp2)にしても発生しない
    5.Class2.hの #pragma pack(push,2)を有効にしたまま
      ソリューションにて構造体アライメントを8(/Zp8)にすると発生する



    なぜ(push,2)の設定をすると変数が壊れてしまうのでしょうか?

    以下サンプルソースです。
    VS2017の15.7.2を利用しています。

    ◆ConsoleApplication.cpp

    #include "Class2.h"
    
    int main()
    {
    	Class2 cClass2;
    	return 0;
    }

    ◆Class1.cpp

    #include "Class1.h"
    
    Class1::Class1(void)
    {
    	m_Class1_iDummy = 0;
    	m_Class1_dWidth = 0.0;
    }
    
    void Class1::Class1_Init()
    {
    	m_Class1_iDummy = 8;
    	m_Class1_dWidth = 8.0;
    }


    ◆Class2.cpp

    #include "Class2.h"
    
    Class2::Class2()
    {
    	m_Class2_Str1= _T("i=");
    
    	//この関数内で m_Class1_dWidth に代入して関数を抜けると
    	//m_Class2_Str1 が壊れる
    	m_Class2_Arrow.Class1_Init();
    }


    ◆Class1.h

    class Class1 {
    public:
    	//コンストラクタ
    	Class1(void);
    
    private:
    	//メンバ
    	int		m_Class1_iDummy;
    	double	m_Class1_dWidth;
    
    public:
    	//メソッド
    	void	Class1_Init(void);
    };

    ◆Class2.h

    #pragma pack(push,2)	//★このpackをコメントアウトすると問題ない
    
    #include "Class1.h"
    #include <tchar.h>
    #include <atlstr.h>  
    
    class Class2
    {
    public:
    	// 標準のコンストラクタ
    	Class2();   
    
    private:
    	Class1	m_Class2_Arrow;
    	CString 	m_Class2_Str1;
    };

    以上、よろしくお願いします。



    • 編集済み kawasima 2018年6月21日 6:45
    2018年6月21日 6:12

回答

  • ConsoleApplication.cpp、Class2.cppをコンパイルする際は

    ConsoleApplication.cpp、Class2.cpp
    → Class2.h
       #pragma pack(push, 2)
      → Class1.h
       class Class1 { ... };

    という順になっているため、ConsoleApplication.cpp、Class2.cppで生成されるコードにおけるClass1は2バイトアラインでコンパイルされます。しかしClass1.cppをコンパイルする際は

    Class1.cpp
    → Class1.h
      class Class1 { ... };

    という順になっているため、Class1はデフォルトアライン(4バイトとか8バイト?)でコンパイルされます。

    結果、両バイナリをリンクするとClass1のメモリレイアウトが矛盾し、データ破壊は発生します。

    Visual C++ 2015以降であればC++11仕様のalignasが使えます。こちらであれば

    class alignas(2) Class2{ ... };

    と記述できます。この方式は影響範囲が明確であり、質問のような問題を引き起こしません。

    とは言え、アラインメントはプラットフォーム毎に適切に設計されていますので、無理に変更しないことをお勧めします。

    • 回答としてマーク kawasima 2018年6月21日 7:21
    2018年6月21日 6:55

すべての返信

  • ConsoleApplication.cpp、Class2.cppをコンパイルする際は

    ConsoleApplication.cpp、Class2.cpp
    → Class2.h
       #pragma pack(push, 2)
      → Class1.h
       class Class1 { ... };

    という順になっているため、ConsoleApplication.cpp、Class2.cppで生成されるコードにおけるClass1は2バイトアラインでコンパイルされます。しかしClass1.cppをコンパイルする際は

    Class1.cpp
    → Class1.h
      class Class1 { ... };

    という順になっているため、Class1はデフォルトアライン(4バイトとか8バイト?)でコンパイルされます。

    結果、両バイナリをリンクするとClass1のメモリレイアウトが矛盾し、データ破壊は発生します。

    Visual C++ 2015以降であればC++11仕様のalignasが使えます。こちらであれば

    class alignas(2) Class2{ ... };

    と記述できます。この方式は影響範囲が明確であり、質問のような問題を引き起こしません。

    とは言え、アラインメントはプラットフォーム毎に適切に設計されていますので、無理に変更しないことをお勧めします。

    • 回答としてマーク kawasima 2018年6月21日 7:21
    2018年6月21日 6:55
  • 佐祐理 様

    早速の回答ありがとうございます。

    コンパイルの順番までは気にしてませんでした。
    説明にある内容にて理解出来ました。

    ありがとうございます。

    2018年6月21日 7:21
  • すこし心配なのでコメントしておきます。
    まず、「コンパイルの順番」は問題ではありません。
    パックされる範囲に"includeによって他の構造が含まれているのが問題です。
    これは意図して施したものではないと考えられます。

    Class2.hにおいて、「#pragma pack(push,2)」の記述がありますが、
    これと対になる「#pragma pack(pop)」の記述が、同ヘッダーにあることを確認しましょう。で、

    (A)この二行の間に定義されたクラス構造体は2Byteのアライメントになります。
    (B)この時、#include<> されたヘッダー内の構造も同じように2Byteのアライメントになることに注意が必要です。

    一般に#pragma pack(push(n))~#pragma pack(pop)は、一つの構造に対して設定すべきで、
    この範囲に#include<>を含むべきではありません(わかってやっているなら別ですが)。
    この意味において、/Zpの出番は「ほぼありえない」という判断にもなります。


    2018年6月21日 7:46