C ++での構造比較演算子の自動生成

すべてのユーザークラスおよび構造のC ++言語は、デフォルトでコピーコンストラクターとコピー割り当て演算子を生成します。 したがって、重要な一連のケースでは、プログラマはこれらの関数を手動で記述する必要がありません。 たとえば、デフォルトのステートメントは、データを含む構造に対して適切に機能します。 さらに、データは単純型とstd :: vectorやstd :: stringなどの複雑なコンテナの両方に保存できます。



これに照らして、デフォルトで== and!=構造比較演算子を使用すると便利ですが、C ++コンパイラは標準に従ってこれらを生成しません。



構造を用語ごとに比較するための演算子を記述することは難しくありませんが、このようなプログラムの編成はエラーの観点からは不便で危険です。 たとえば、プログラマーが構造に新しいメンバーを追加したが、ユーザー比較演算子で対応する比較を追加するのを忘れると、かなり難しい診断エラーがプログラムで生成されます。 さらに、構造の宣言とその比較のユーザー演算子は、異なるファイル(* .hおよび* .cpp)にあるため、通常は互いに分離されています。



C ++での単語ごとの比較演算子の記述を自動化するのは簡単ではありません。この言語には、プログラム操作中に構造に含まれるメンバーの数とメンバーを調べるツールがないためです。



2000年代半ばに、絶えず進化し、データ構造の頻繁な変更を必要とする大規模なプロジェクトに取り組み、比較演算子の問題を完全に解決することに着手しました。 その結果、マクロを使用してC ++コンストラクトが作成されました。これにより、その後の用語ごとの比較演算子の自動生成で構造を宣言できます。 同じ設計により、他のセマンティック操作を自動的に実装することができました:ファイルへのデータのロードと保存。 あなたの注意を喚起します。

他の既存のソリューション


現時点では、説明した問題に対する次の代替ソリューションを知っています。

  1. 動的構造の使用。 通常のC ++構造の代わりに、単一の型にキャストされる異種要素のコンテナが適用されます。 たとえば、Windows OLEからVARIANTと入力します。 文字列コンテナは、メンバー名の保存にも使用されます。 したがって、メンバーの名前、そのタイプ、および番号は、実行時にプログラムで使用可能になります。 ただし、このアプローチでは、そのような構造のメンバーにアクセスするためのプログラムの実行中にコストが発生します。 object.member_nameまたはpObject-> member_nameの形式のアクセス構文は使用できなくなり、object.at(“ member_name”)のようなものに変更する必要があります。 さらに、メモリ消費が直線的に増加します。構造の各インスタンスは、通常の(静的)構造よりも多くのメモリ領域を占有します。
  2. boostライブラリ、つまりboost :: fusion :: map containerを使用します。 ここでは、すべてのコストをコンパイラの肩にかけることができましたが、メンバーにアクセスするための従来の構文は保存できませんでした。 次の形式の構造を使用する必要があります:at_key <member_name>(object)。
  3. C ++コード生成。 C ++での構造の説明とその比較の演算子は、プログラマーが手動で作成するのではなく、他の入力言語での構造の説明に基づいたスクリプトによって生成されます。 私の観点からは、このアプローチは理想的ですが、まだ実装していないため、この記事は彼に関するものではありません。


マクロソリューション


マクロを使用して実装できたソリューションには、次の利点があります。



欠点のうち、次の点に注意してください。



使用例


次の通常の構造と同等の、人々に関するデータを保存するための構造を作成する必要があるとします。

struct MANPARAMS { std::string name; int age; std::vector<std::string> friend_names; double karma; };
      
      





私のライブラリに基づいて、自動用語操作を含む構造体は次のように宣言されます。

 class AUTO_MANPARAMS { PARAMSTRUCT_DECLARE_BEGIN(AUTO_MANPARAMS); public: DECLARE_MEMBER_PARAMSTRUCT(std::string, name); DECLARE_MEMBER_PARAMSTRUCT(int, age); DECLARE_MEMBER_PARAMSTRUCT(std::vector<std::string>, friend_names); DECLARE_MEMBER_PARAMSTRUCT(double, karma); };
      
      





その後、プログラム全体に対して、* .cppファイルのいずれかで次のマクロ呼び出しをコンパイルする必要があります。

 PARAMFIELD_IMPL(AUTO_MANPARAMS);
      
      





それだけです! これで、これらの構造体を通常どおり安全に使用し、適切な演算子を記述することを心配せずに、それらの構造が等しいかどうかを比較できます。 例:

 void men(void) { AUTO_MANPARAMS man1, man2; man1.name = “John Smith”; man1.age = 18; man1.karma = 0; man2.name = “John Doe”; man2.age = 36; man2.karma = 1; man2.friends.push_back(“Sergud Smith”); if(man1 == man2) printf(“Ku-ku!\n”); }
      
      





実装


上記からわかるように、各構造の定義の開始時に、PARAMSTRUCT_DECLARE_BEGIN(x)マクロを呼び出す必要があります。このマクロは、この構造の一般的なタイプと静的サービスメンバーを決定します。 その後、各ユーザーメンバーを宣言するときに、2番目のマクロDECLARE_MEMBER_PARAMSTRUCT(タイプ、名前)を呼び出します。これは、指定された名前を持つメンバーの宣言に加えて、それに関連付けられた構造のサービスメンバーを定義します。



主要な実装のアイデア:



1.各メンバーの比較関数の自動生成


このような各関数は構造体のメンバーであり、「その」データメンバーを比較します。 マクロDECLARE_MEMBER_PARAMSTRUCT(タイプ、名前)で次のように生成されます。

 bool comp##name(const ThisParamFieldClass& a) const \ { \ return name == a.name; \ } \
      
      





ThisParamFieldClassは構造体の型で、ヘッドマクロのtypedefを介して宣言されます-以下を参照してください。



2.比較関数へのポインタを持つ配列


ヘッドマクロPARAMSTRUCT_DECLARE_BEGIN(x)は、各メンバー比較関数へのポインターが格納される静的配列を宣言します。 これを行うには、最初にそれらのタイプを決定します。

 #define PARAMSTRUCT_DECLARE_BEGIN(x) \ private: \ typedef x ThisParamFieldClass; \ typedef bool (ThisParamFieldClass::*ComFun)(const ThisParamFieldClass& a) const; \ struct MEM_STAT_DATA \ { \ std::string member_name; \ ComFun comfun; \ }; \
      
      





そして、配列が宣言されます:



 static std::vector<MEM_STAT_DATA> stat_data; \
      
      







比較演算子もここで宣言されています:

 public: \ bool operator==(const ThisParamFieldClass& a) const; \ bool operator!=(const ThisParamFieldClass& a) const { return !operator==(a); } \
      
      





比較演算子は別のマクロ(PARAMFIELD_IMPL)によって実装されますが、その実装はstat_data配列が存在する場合は簡単です。この配列の各要素に対して比較関数を呼び出すだけです。

構造比較のみの場合、構造メンバーの名前を配列に保存する必要はありません。 ただし、名前を保存すると、概念を拡張して用語比較だけでなく、他の操作、たとえば人間が読むのに適したテキスト形式で保存およびロードすることにも適用できます。



3.構造体のメンバーに関するデータの入力


配列stat_dataを埋める問題を解決するために残っています。 メンバー情報はDECLARE_MEMBER_PARAMSTRUCTマクロ以外の場所では最初は利用できないため、そこから(直接または間接的に)配列にデータを追加することしかできません。 ただし、このマクロは構造宣言内で呼び出されます。これは、std :: vectorを初期化するのに最も便利な場所ではありません。 この問題はサービスオブジェクトで解決しました。 このクラスのサービスクラスとオブジェクトは、構造体の各メンバーに対して宣言されます。 このクラスにはコンストラクタがあります-要素に関する情報をstat_data静的配列に追加します。

 class cl##name \ { \ public: \ cl##name(void) \ { \ if(populate_statdata) \ { \ MEM_STAT_DATA msd = \ { \ #name, \ &ThisParamFieldClass::comp##name \ }; \ stat_data.push_back(msd); \ } \ } \ }; \ cl##name ob##name;
      
      







populate_statdataは静的フラグで、ヘッドマクロで宣言され、stat_data配列に構造体メンバーとその比較関数の名前を入力する必要があるかどうかを通知します。 プログラムが起動すると、以下で説明する初期化メカニズムがpopulate_statdata = trueを設定し、構造体の1つのインスタンスを作成します。 同時に、構造の各メンバーに関連付けられたサービスオブジェクトコンストラクターは、配列にメンバーデータを格納します。 その後、populate_statdata = falseが設定され、メンバー情報を持つ静的配列は変更されなくなります。 このソリューションは、ユーザープログラムがpopulate_statdataフラグをチェックするための構造を作成するたびに、ある程度の時間損失につながります。 ただし、メモリ消費は増加しません。サービスオブジェクトにはデータメンバは含まれず、コンストラクタのみが含まれます。



最後に、populate_statdata:フラグ制御メカニズムは、構造体全体のコンストラクターを持つ静的サービスオブジェクトを使用して実装されます。 このオブジェクトは、ヘッドマクロで宣言されています。

 class VcfInitializer \ { \ public: \ VcfInitializer(void); \ }; \ static VcfInitializer vcinit;
      
      





コンストラクターの実装は、PARAMFIELD_IMPL(x)マクロにあります。

 x::VcfInitializer::VcfInitializer(void) \ { \ x::populate_statdata = true; \ ThisParamFieldClass dummy; \ x::populate_statdata = false; \ } \
      
      







完全なマクロテキスト


 #define PARAMSTRUCT_DECLARE_BEGIN(x) \ private: \ typedef x ThisParamFieldClass; \ typedef bool (ThisParamFieldClass::*ComFun)(const ThisParamFieldClass& a) const; \ struct MEM_STAT_DATA \ { \ std::string member_name; \ ComFun comfun; \ }; \ static std::vector<MEM_STAT_DATA> stat_data; \ static bool populate_statdata; \ public: \ bool operator==(const ThisParamFieldClass& a) const; \ bool operator!=(const ThisParamFieldClass& a) const { return !operator==(a); } \ private: \ class VcfInitializer \ { \ public: \ VcfInitializer(void); \ }; \ static VcfInitializer vcinit; #define DECLARE_MEMBER_PARAMSTRUCT(type,name) \ public: \ type name; \ private: \ bool comp##name(const ThisParamFieldClass& a) const \ { \ return name == a.name; \ } \ class cl##name \ { \ public: \ cl##name(void) \ { \ if(populate_statdata) \ { \ MEM_STAT_DATA msd = \ { \ #name, \ &ThisParamFieldClass::comp##name, \ }; \ stat_data.push_back(msd); \ } \ } \ }; \ cl##name ob##name; #define PARAMFIELD_IMPL(x) \ std::vector<x::MEM_STAT_DATA> x::stat_data; \ bool x::populate_statdata = false; \ x::VcfInitializer x::vcinit; \ x::VcfInitializer::VcfInitializer(void) \ { \ x::populate_statdata = true; \ ThisParamFieldClass dummy; \ x::populate_statdata = false; \ } \ bool x::operator==(const x& a) const \ { \ bool r = true; \ for(size_t i=0; r && i<stat_data.size(); i++) \ { \ r = (this->*stat_data[i].comfun)(a); \ } \ return r; \ }
      
      





おわりに


上記のマクロに基づいて、比較演算子およびその他の用語操作が自動的に作成される構造を宣言できます。 他のこのような操作には、たとえば、XML形式でのテキストファイルの読み込みと保存が含まれます。 コードの重複がないため、作業が簡単になり、エラーが防止されます。 構造体のメンバーを宣言するだけで、そのメンバーが比較、保存、および読み込み操作に追加されます。



All Articles