Как сравнить общие структуры в 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и многие другие).

idclev 463035818
источник
1
Мне нравится этот ответ. С этим типом черты вы можете использовать SFINAE для использования memcmpна структурах без заполнения и выполнять operator==только при необходимости.
Иксисарвинен
Хорошо, спасибо. С этим я могу с уверенностью заключить, что мне нужно сделать некоторые ручные размышления.
Фредрик Энеторп
6

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

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

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

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

n314159
источник
Хороший хак! К сожалению, я застрял с 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;
}
selbie
источник
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; 
}
Kostas
источник
@walnut Ты прав, это был ужасный ответ. Переписал один.
Костас
Гарантирует ли стандарт, что присвоение оставляет байты заполнения нетронутыми? Также все еще существует проблема множественных представлений объектов для одного и того же значения в фундаментальных типах.
грецкий орех
@walnut Я верю, что это так .
Костас
1
Комментарии под верхним ответом в этой ссылке, кажется, указывают, что это не так. Сам ответ только говорит о том , что заполнение не обязательно копировать, но не то, что он musn't . Хотя я точно не знаю.
грецкий орех
Я сейчас проверил это, и это не работает. Назначение не оставляет байтов заполнения нетронутыми.
Фредрик Энеторп
0

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

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

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

einpoklum
источник