Как сравнить общие структуры в 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;
}

В основном это работает так, как задумано, за исключением того, что иногда оно возвращает false, даже если два экземпляра структуры имеют идентичные члены (я проверял с помощью отладчика eclipse). После некоторого поиска я обнаружил, что он memcmpможет потерпеть неудачу из-за заполнения используемой структуры.

Есть ли более правильный способ сравнения памяти, который безразличен для заполнения? Я не могу изменить используемые структуры (они являются частью API, который я использую), и многие различные используемые структуры имеют несколько различных членов и, следовательно, не могут сравниваться по отдельности в общем виде (насколько мне известно).

Изменить: я, к сожалению, застрял с C ++ 11. Должен был упомянуть об этом раньше ...


Можете ли вы показать пример, где это не удается? Заполнение должно быть одинаковым для всех экземпляров одного типа, нет?
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;
};

мы хотим выполнить минимальный объем работы, чтобы мы могли сравнить два из них.

Если мы имеем:

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) (полностью рекурсивный, даже поддерживает массивы-массивы):

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может сэкономить на издержках времени компиляции, так как crefэто намного более простой класс, чем tuple.

Наконец, вы должны добавить

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. Есть ли шанс, что вы можете помочь мне с этим?
Фредрик Энеторп

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. Или явно указать тип возвращаемого значения.
Дархуук

1
@FredrikEnetorp Исправлено, плюс макрос, который облегчает написание. Работа над тем, чтобы заставить его работать полностью рекурсивно (так что структура-структура, где подструктуры имеют as_tieподдержку, автоматически работает) и поддержку элементов массива, не детализирована, но это возможно.
Якк - Адам Невраумонт

Спасибо. Я делал ужасные макросы немного по-другому, но функционально эквивалентно. Еще одна проблема. Я пытаюсь обобщить сравнение в отдельном заголовочном файле и включить его в различные тестовые файлы gmock. Это приводит к сообщению об ошибке: множественное определение `as_tie (Test1 const &) 'Я пытаюсь встроить их, но не могу заставить его работать.
Фредрик Энеторп

1
@FredrikEnetorp inlineКлючевое слово должно убрать несколько ошибок определения. Используйте кнопку [задать вопрос] после того, как вы получите минимальный воспроизводимый пример
Якк - Адам Невраумонт

7

Вы правы, что заполнение мешает вам сравнивать произвольные типы таким способом.

Есть меры, которые вы можете предпринять:

  • Если вы контролируете, Dataто, например, gcc имеет __attribute__((packed)). Это влияет на производительность, но, возможно, стоит попробовать. Тем не менее, я должен признать, что не знаю, packedпозволяет ли вам полностью запретить заполнение. ГКК док говорит:

Этот атрибут, прикрепленный к определению типа структуры или объединения, указывает, что каждый член структуры или объединения размещается для минимизации требуемой памяти. При присоединении к определению перечисления это указывает, что следует использовать наименьший целочисленный тип.

  • Если вы не контролируете ситуацию, Dataто, по крайней мере, std::has_unique_object_representations<T>можете сказать, даст ли ваше сравнение правильные результаты:

Если T является TriviallyCopyable и если любые два объекта типа T с одинаковым значением имеют одинаковое представление объекта, значение константы члена равно true. Для любого другого типа значение равно false.

и далее:

Эта черта была введена, чтобы дать возможность определить, можно ли правильно хэшировать тип, хэшируя его представление объекта в виде байтового массива.

PS: я обращался только к отступам, но не забывайте, что типы, которые могут сравниваться одинаково для экземпляров с разным представлением в памяти, ни в коем случае не редкость (например std::string, std::vectorи многие другие).


1
Мне нравится этот ответ. С этим типом черты вы можете использовать SFINAE для использования memcmpна структурах без заполнения и выполнять operator==только при необходимости.
Иксисарвинен

Хорошо, спасибо. С этим я могу с уверенностью заключить, что мне нужно сделать некоторые ручные размышления.
Фредрик Энеторп

6

Вкратце: не возможно в общем смысле.

Проблема с memcmp в том, что заполнение может содержать произвольные данные, и, следовательно, memcmpможет произойти сбой. Если бы был способ выяснить, где находится заполнение, вы могли бы обнулить эти биты и затем сравнить представления данных, что проверило бы на равенство, если бы члены были тривиально сопоставимы (что не так, т. Е. std::stringПоскольку две строки могут содержат разные указатели, но два указанных массива одинаковы). Но я не знаю способа добраться до заполнения структур. Вы можете попытаться указать компилятору упаковать структуры, но это замедлит доступ и на самом деле не гарантирует его работу.

Самый простой способ реализовать это - сравнить всех участников. Конечно, это не возможно в общем виде (пока мы не получим отражения времени компиляции и мета-классы в C ++ 23 или более поздней версии). Начиная с C ++ 20, можно создавать значения по умолчаниюoperator<=> но я думаю, что это также возможно только в качестве функции-члена, поэтому, опять же, это не совсем применимо. Если вам повезло, и у всех структур, которые вы хотите сравнить, есть operator==определенные, вы, конечно, можете просто использовать это. Но это не гарантировано.

РЕДАКТИРОВАТЬ: Хорошо, на самом деле есть совершенно хакерский и несколько общий способ для агрегатов. (Я только написал преобразование в кортежи, у них есть оператор сравнения по умолчанию). godbolt


Хороший хак! К сожалению, я застрял с C ++ 11, поэтому я не могу его использовать.
Фредрик Энеторп

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 по умолчанию, OP не смог бы их использовать, так как по умолчанию операторы ==or <=>могут быть выполнены только на уровне класса.
Николь Болас

Как сказал Ник Болас, я не могу изменить структуру.
Фредрик Энеторп

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 Я верю, что это так .
Костас

1
Комментарии под верхним ответом в этой ссылке, кажется, указывают, что это не так. Сам ответ только говорит о том , что заполнение не обязательно копировать, но не то, что он musn't . Хотя я точно не знаю.
грецкий орех

Я сейчас проверил это, и это не работает. Назначение не оставляет байтов заполнения нетронутыми.
Фредрик Энеторп

0

Я полагаю, что вы можете основать решение на удивительно коварном вуду Антония Полухина в magic_getбиблиотеке - для структур, а не для сложных классов.

С помощью этой библиотеки мы можем перебирать различные поля структуры с их соответствующим типом в чисто общем шаблонном коде. Энтони, например, использовал это, чтобы иметь возможность потоковой передачи произвольных структур в выходной поток с правильными типами, совершенно обобщенно. Само собой разумеется, что сравнение также может быть возможным применением этого подхода.

... но вам нужен C ++ 14. По крайней мере, это лучше, чем C ++ 17 и последующие предложения в других ответах :-P

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.