【C言語/C++】構造体を比較する方法【安全な比較と比較関数の定義】

C言語やC++では、比較演算子による構造体同士の比較は行なえません。代わりに構造体のメンバ変数を比較する必要があります。実際の開発シーンでは比較関数を作成することが一般的です。ちなみにC++では比較演算子を独自定義することが可能です。

構造体を比較する方法

構造体は比較演算子による比較処理が行なえません。

struct Date { int year, month, day; };

int main() {
   struct Date a = {2017, 8, 13};
   struct Date b = {2017, 8, 13};
   if (a == b) {} // Invalid operands to binary expression ('struct Date' and 'struct Date')
}

そのため、構造体同士の比較を行う場合には、構造体のメンバ変数を個別に比較する必要があります。

struct Date a = {2017, 8, 13};
struct Date b = {2017, 8, 13};

if (a.year  == b.year  &&
    a.month == b.month &&
    a.day   == b.day) {
   puts("same");
}

ただし、構造体のメンバ変数が新たに追加された場合の対応を考慮し、実際の開発では構造体比較用の関数を定義することをオススメします。

構造体を比較するための関数を定義する方法

構造体の比較を実現するための関数の定義方法です。

struct Date { int year, month, day; };

// 比較関数
bool Date_equal(struct Date* a, struct Date *b) {
   return a->year  == b->year  &&
          a->month == b->month &&
          a->day   == b->day;
}
int main() {
   struct Date a = {2017, 8, 13};
   struct Date b = {2017, 8, 13};
   if (Date_equal(&a, &b)) { puts("same"); }
}

このような比較関数を汎用的に利用することで、メンバ変数が新たに追加された場合の対応が容易になります。関数側の定義を修正するだけで済むため、既存のコードの修正を最小限に抑えることができます。

ポインタ渡しと値渡しについて

構造体メンバ変数の数が極端に多い場合は、メンバ変数の値コピーのコストを抑えるために、比較関数の引数をポインタ変数として受け取るようにするとよいでしょう。今回紹介した例がそれにあたります。

bool Date_equal(struct Date* a, struct Date *b);

ただし関数のインライン展開が有効な環境や、メンバーの数が比較的少ない構造体では、値渡しを用いた比較関数を定義することも有効です。

bool Date_equal(struct Date a, struct Date b);

以下は値渡しによる比較関数の具体的な例です。ポインタ渡しの例と比べると、より簡潔な記述になっていることがわかります。

struct Range { int location, length; };

inline bool Range_equal(struct Range a, struct Range b) {
   return a.location == b.location && a.length == b.length;
}

int main() {
   struct Range a = {2, 4};
   struct Range b = {2, 4};
   if (Range_equal(a, b)) { puts("same"); }
}

単純なデータ構造を持った構造体を扱う場合は、値渡しを活用したほうが利便性は高くなる傾向にあります。

// Before
struct Range range = {2, 4};
struct Range zero = {0, 0};
if (!Range_equal(&range, &zero)) { puts("not empty"); }

// After
struct Range range = {2, 4};
if (!Range_equal(range, Range_make(0, 0))) { puts("not empty"); }

// if (!Range_equal(range, (struct Range){0, 0})) { puts("not empty"); }
// struct Range Range_make(int loc, int len) { return (struct Range){loc, len}; }

ただし、対象の構造体がポインタとしてやり取りされることが多いような場合には、従来のようにポインタ渡しを用いた比較関数を定義するようにしましょう。

memcmpによる比較について

C言語の世界にはmemcmp関数を活用して、構造体をバイト単位で比較する特殊なテクニックが存在します。

struct Date a = {2017, 8, 13}, b = {2017, 8, 13};
// NG
if (memcmp(&a, &b, sizeof(struct Date)) == 0) puts("same");

バイト単位の比較は簡潔な記述と高速な処理を実現することもありますが、実際には非推奨の処理ですので、使用しないよう注意してください。

構造体型変数のデータ領域には、メンバ変数へのアライメントによって、不定なパディング領域が割り当てられることがあるためです。不定な領域同士の比較は偽の比較結果をもたらすことに繋がります。

アライメントとバイト比較の危険性

例えば、以下の構造体型Tchar型(1バイト)とsize_t型(8バイト)のメンバを有する構造体ですが、多くの環境では構造体自体のサイズは9バイトではなく16バイトとなってしまいます。

struct T { char c; size_t i; };
assert( sizeof(T) == 16 );

アライメントによってメンバ変数char c;の後に7バイト分のパディング領域が割り当てられるためです(これによってメンバsize_t i;は4の倍数または8の倍数のアドレスに配置されることになります。コンピュータにとってはそのほうが処理がしやすく都合の良い配置となります)。なお余計に割り当てられた7バイト分の領域には、変数宣言時に不定な値が格納されることになります。

memcmp関数による構造体の比較テクニックは、そのような不定なパディング領域同士の比較を招く可能性があり、見つかりにくいバグを引き起こす危険な処理となります。

アライメントの方法はメンバ型の種類や並び方、実行環境(32bit/64bit)によっても大きく変化するため、このような不安定なコードは回避する必要があります。

比較演算子の定義

C++の場合は比較演算子の多重定義が可能となっています(演算子のオーバーロード)。この機能によって、構造体同士の比較処理を実現することができます。

struct Number {
  int value;
  bool operator==(const Number& x) { return value == x.value; }
  bool operator!=(const Number& x) { return value != x.value; }
};

int main() {
  Number a = {9}, b = {9};
  if (a == b) puts("yes");                  // "yes"
  if (a != b) puts("yes"); else puts("no"); // "no"
}

演算子オーバロードは非メンバ関数として定義することも可能です。

// クラスの定義
struct Number { int value; };
// 演算子の定義
bool operator==(const Number& a, const Number& b) { return a.value == b.value; }
bool operator!=(const Number& a, const Number& b) { return a.value != b.value; }

非メンバ関数による定義方法であれば、既存のAPIやC言語スタイルの構造体に対して、比較処理を独自に実装することも可能となります。

inline bool operator==(const Range& a, const Range& b) {
   return Range_equal(a, b);
}
// Before
Range_equal(Range_make(0, 2), Range_make(0, 2));
// After
Range_make(0, 2) == MakeRange(0, 2);
広告