C ++でジェネリック構造体を比較する方法は?


13

構造体を一般的な方法で比較したいのですが、次のようなことをしました(実際のソースを共有できないため、必要に応じて詳細を尋ねます)。

template<typename Data>
bool structCmp(Data data1, Data data2)
{
  void* dataStart1 = (std::uint8_t*)&data1;
  void* dataStart2 = (std::uint8_t*)&data2;
  return memcmp(dataStart1, dataStart2, sizeof(Data)) == 0;
}

2つの構造体インスタンスに同じメンバーが含まれている場合(ただし、Eclipseデバッガーで確認しました)でもfalseを返す場合があることを除いて、これはほとんど意図どおりに機能します。いくつか検索した後memcmp、使用された構造体が埋め込まれているために失敗する可能性があることを発見しました。

パディングに無関心なメモリを比較するより適切な方法はありますか?使用する構造体を変更することはできません(使用しているAPIの一部です)。使用する多くの異なる構造体にはいくつかの異なるメンバーがあるため、一般的な方法で個別に比較することはできません(私の知る限り)。

編集:残念ながら、C ++ 11を使用しています。これは以前に言及したはずです...


これが失敗する例を示すことができますか?パディングは1つのタイプのすべてのインスタンスで同じである必要がありますか?
idclev 463035818

1
@ idclev463035818パディングは指定されていません。その値を想定することはできません。それを読み込もうとするのはUBだと思います(最後の部分はわかりません)。
フランソワ・アンドリュー

@ idclev463035818パディングはメモリ内の同じ相対的な場所にありますが、データが異なる場合があります。これは、構造体の通常の使用では破棄されるため、コンパイラーがわざわざゼロにすることはありません。
NO_NAME

2
@ idclev463035818パディングは同じサイズです。そのパディングを構成するビットの状態は何でもかまいません。あなたはときにmemcmp、あなたの比較において、これらのパディングビットを含みます。
フランソワアンドリュー

1
Yksisarvinenに同意します。構造体ではなくクラスを使用し、==演算子を実装します。使用memcmpは信頼性が低く、遅かれ早かれ、「他のクラスとは少し異なる方法で」行う必要があるクラスを扱うことになります。それをオペレーターに実装することはとてもクリーンで効率的です。実際の振る舞いは多態的ですが、ソースコードはクリーンです...そして明白です。
マイクロビンソン

回答:


7

いいえ、memcmpこれを行うには適していません。また、C ++でのリフレクションでは、現時点ではこれを行うには不十分です(これを行うのに十分なリフレクションをサポートする実験的なコンパイラがすでにあり、必要な機能がある場合があります)。

組み込みのリフレクションがない場合、問題を解決する最も簡単な方法は、手動でリフレクションを行うことです。

これを取る:

struct some_struct {
  int x;
  double d1, d2;
  char c;
};

これらの2つを比較できるように、最小限の作業を行います。

私たちが持っている場合:

auto as_tie(some_struct const& s){ 
  return std::tie( s.x, s.d1, s.d2, s.c );
}

または

auto as_tie(some_struct const& s)
-> decltype(std::tie( s.x, s.d1, s.d2, s.c ))
{
  return std::tie( s.x, s.d1, s.d2, s.c );
}

以下のための、次のようになります。

template<class S>
bool are_equal( S const& lhs, S const& rhs ) {
  return as_tie(lhs) == as_tie(rhs);
}

かなりまともな仕事をします。

このプロセスを拡張して、少しの作業で再帰的にすることができます。タイを比較するのではなく、テンプレートにラップされた各要素を比較します。要素がすでに動作している場合を除いて、テンプレートはoperator==再帰的にこのルールを適用(要素をラップしas_tieて比較)し、==配列を処理します。

これには、少しのライブラリ(コードの100行)と、メンバーごとの手動の "リフレクション"データを少し書き込む必要があります。使用する構造体の数が限られている場合は、構造体ごとのコードを手動で記述する方が簡単な場合があります。


おそらく取得する方法があります

REFLECT( some_struct, x, d1, d2, c )

as_tie恐ろしいマクロを使用して構造を生成する。しかし、as_tie十分に簡単です。ではの繰り返しは迷惑です。これは便利です:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

この状況や他の多くの状況で。でRETURNS、書くことas_tieは:

auto as_tie(some_struct const& s)
  RETURNS( std::tie( s.x, s.d1, s.d2, s.c ) )

繰り返しを削除します。


これを再帰的にする試みは次のとおりです。

template<class T,
  typename std::enable_if< !std::is_class<T>{}, bool>::type = true
>
auto refl_tie( T const& t )
  RETURNS(std::tie(t))

template<class...Ts,
  typename std::enable_if< (sizeof...(Ts) > 1), bool>::type = true
>
auto refl_tie( Ts const&... ts )
  RETURNS(std::make_tuple(refl_tie(ts)...))

template<class T, std::size_t N>
auto refl_tie( T const(&t)[N] ) {
  // lots of work in C++11 to support this case, todo.
  // in C++17 I could just make a tie of each of the N elements of the array?

  // in C++11 I might write a custom struct that supports an array
  // reference/pointer of fixed size and implements =, ==, !=, <, etc.
}

struct foo {
  int x;
};
struct bar {
  foo f1, f2;
};
auto refl_tie( foo const& s )
  RETURNS( refl_tie( s.x ) )
auto refl_tie( bar const& s )
  RETURNS( refl_tie( s.f1, s.f2 ) )

refl_tie(array)(完全に再帰的、array-of-arraysもサポート):

template<class T, std::size_t N, std::size_t...Is>
auto array_refl( T const(&t)[N], std::index_sequence<Is...> )
  RETURNS( std::array<decltype( refl_tie(t[0]) ), N>{ refl_tie( t[Is] )... } )

template<class T, std::size_t N>
auto refl_tie( T(&t)[N] )
  RETURNS( array_refl( t, std::make_index_sequence<N>{} ) )

ライブの例

ここで私は使うstd::arrayのをrefl_tie。これは、コンパイル時に私の以前のrefl_tieのタプルよりもはるかに高速です。

また

template<class T,
  typename std::enable_if< !std::is_class<T>{}, bool>::type = true
>
auto refl_tie( T const& t )
  RETURNS(std::cref(t))

std::cref代わりにここを使用すると、よりもはるかに単純なクラスのstd::tieように、コンパイル時のオーバーヘッドを節約できます。creftuple

最後に、追加する必要があります

template<class T, std::size_t N, class...Ts>
auto refl_tie( T(&t)[N], Ts&&... ) = delete;

これは、配列のメンバーがポインターに減衰し、ポインターの等価性にフォールバックするのを防ぎます(おそらく配列からは不要です)。

これがなければ、配列を非反映構造体に渡すと、非反映構造体へのポインターにフォールバックしますrefl_tie。これは機能し、ナンセンスを返します。

これにより、コンパイル時エラーが発生します。


ライブラリタイプによる再帰のサポートは注意が必要です。あなたはstd::tieそれらをすることができます:

template<class T, class A>
auto refl_tie( std::vector<T, A> const& v )
  RETURNS( std::tie(v) )

しかし、それはそれを介した再帰をサポートしていません。


手動での反射によるこの種の解決策を追求したいと思います。あなたが提供したコードはC ++ 11では動作しないようです。何かお手伝いできることはありませんか?
Fredrik Enetorp

1
これがC ++ 11で機能しない理由は、に後続の戻り型がないためですas_tie。C ++ 14以降、これは自動的に推定されます。auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));C ++ 11で使用できます。または、戻り値の型を明示的に示します。
Darhuuk

1
@FredrikEnetorp修正済み、および簡単に記述できるマクロ。完全に再帰的に機能し(サブ構造体がas_tieサポートするstruct-of-structが自動的に機能するように)、配列メンバーをサポートするための作業は詳細ではありませんが、可能です。
Yakk-Adam Nevraumont

ありがとうございました。私は恐ろしいマクロを少し異なって行いましたが、機能的には同等です。もう1つ問題があります。別のヘッダーファイルで比較を一般化し、さまざまなgmockテストファイルに含めようとしています。その結果、エラーメッセージが表示されます。`as_tie(Test1 const&) 'の複数の定義インライン化しようとしていますが、機能させることができません。
Fredrik Enetorp

1
@FredrikEnetorpこのinlineキーワードにより、複数の定義エラーが解消されます。最小限の再現可能な例
Yakk-Adam Nevraumont

7

この方法で任意の型を比較す​​るときに、パディングが邪魔になるのは正しいことです。

実行できる対策があります。

  • あなたが制御しているならData、例えばgccは持ってい__attribute__((packed))ます。パフォーマンスに影響しますが、試してみる価値はあります。ただし、packedパディングを完全に禁止できるかどうかはわかりません。Gcc docは言う:

構造体または共用体タイプの定義に添付されているこの属性は、構造体または共用体の各メンバーを配置して、必要なメモリを最小限に抑えることを指定します。列挙型定義にアタッチされている場合、最小の整数型を使用する必要があることを示します。

  • あなたが制御していない場合Dataは、少なくともstd::has_unique_object_representations<T>あなたの比較が正しい結果をもたらすかどうかを伝えることができます:

TがTriviallyCopyableであり、同じ値を持つタイプTの2つのオブジェクトが同じオブジェクト表現を持つ場合、メンバー定数値はtrueになります。その他のタイプの場合、値はfalseです。

そしてさらに:

この特性は、オブジェクト表現をバイト配列としてハッシュすることにより、型を正しくハッシュできるかどうかを判断できるようにするために導入されました。

PS:私はパディングを取り上げたが、メモリ内の別の表現を持つインスタンスのために等しい比較できるタイプはありません稀な手段(例えばであることを忘れてはいけないstd::stringstd::vectorと多くの他)。


1
私はこの答えが好きです。このタイプの特性を使用すると、SFINAEを使用memcmpしてパディングなしの構造体で使用しoperator==、必要な場合にのみ実装できます。
Yksisarvinen

わかりました。これにより、手動で反映する必要があると安全に結論付けることができます。
Fredrik Enetorp

6

つまり、一般的な方法では不可能です。

の問題memcmpは、パディングに任意のデータが含まれる可能性があるため、memcmpが失敗する可能性があることです。パディングがどこにあるかを見つける方法があった場合、それらのビットをゼロにしてからデータ表現を比較することができます。これにより、メンバーが自明に比較可能かどうか(つまりstd::string、2つの文字列が異なるポインターが含まれていますが、指定された2つのchar配列は同じです)。しかし、私は構造体のパディングを取得する方法を知りません。コンパイラーに構造体をパックするように指示することもできますが、これによりアクセスが遅くなり、実際の動作が保証されません。

これを実装する最もクリーンな方法は、すべてのメンバーを比較することです。もちろん、これは一般的な方法では実際には不可能です(C ++ 23以降でコンパイル時のリフレクションとメタクラスを取得するまで)。C ++ 20以降、デフォルトを生成operator<=>できるようになりましたが、これもメンバー関数としてのみ可能であるため、これは実際には適用できません。あなたが幸運で、比較したいすべての構造体にoperator==定義がある場合は、もちろんそれを使用できます。しかし、それは保証されていません。

編集:わかりました、実際には集計のための完全にハッキーでやや一般的な方法があります。(タプルへの変換だけを書いた、それらにはデフォルトの比較演算子がある)。ゴッドボルト


いいハック!残念ながら、私はC ++ 11で立ち往生しているため、使用できません。
Fredrik Enetorp

2

C ++ 20はデフォルトのコマパリソンをサポートします

#include <iostream>
#include <compare>

struct XYZ
{
    int x;
    char y;
    long z;

    auto operator<=>(const XYZ&) const = default;
};

int main()
{
    XYZ obj1 = {4,5,6};
    XYZ obj2 = {4,5,6};

    if (obj1 == obj2)
    {
        std::cout << "objects are identical\n";
    }
    else
    {
        std::cout << "objects are not identical\n";
    }
    return 0;
}

1
これは非常に便利な機能ですが、質問されたとおりの回答にはなりません。OPは「使用された構造体を変更することはできません」と言っていました。つまり、C ++ 20のデフォルトの等価演算子が使用可能であっても、==or <=>演算子のデフォルト設定しか実行できないため、OPはそれらを使用できません。クラスのスコープで。
ニコルボーラス

ニコル・ボーラスが言ったように、私は構造体を変更することはできません。
Fredrik Enetorp

1

PODデータを想定すると、デフォルトの代入演算子はメンバーバイトのみをコピーします。(実際には100%確実ではありません。私の言葉を信じないでください)

これを有利に使用できます。

template<typename Data>
bool structCmp(Data data1, Data data2) // Data is POD
{
  Data tmp;
  memcpy(&tmp, &data1, sizeof(Data)); // copy data1 including padding
  tmp = data2;                        // copy data2 only members
  return memcmp(&tmp, &data1, sizeof(Data)) == 0; 
}

@walnutそうですね、ひどい答えでした。書き直しました。
コスタ

標準は、割り当てがパディングバイトをそのまま残すことを保証しますか?基本的な型の同じ値に対する複数のオブジェクト表現についての懸念もあります。
ウォールナット

@walnut私はそうだと信じています。
Kostas

1
そのリンクの一番上の回答の下にあるコメントは、そうではないことを示しているようです。答え自体はパディングがいることを言う必要はないが、コピーではなく、それがあることことmusn't。確かに私もわかりません。
ウォールナット

私は今それをテストしました、そしてそれは働きません。割り当てによって、パディングバイトがそのまま残ることはありません。
Fredrik Enetorp

0

magic_getライブラリ内のAntony Polukhinの驚くほどだまされやすいブードゥーに基づいてソリューションを作成できると思います。複雑なクラスではなく、構造体についてです。

そのライブラリーを使用すると、純粋に汎用テンプレート化されたコードで、構造体のさまざまなフィールドを適切なタイプで反復処理できます。Antonyはこれを使用して、たとえば、任意の構造体を正しいタイプの出力ストリームに完全に総称的にストリーミングできるようにしました。比較もこのアプローチの可能なアプリケーションである可能性があることは当然のことです。

...しかし、C ++ 14が必要です。少なくとも、C ++ 17や他の回答での提案より優れています:-P

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.