Як порівняти родові структури в 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;
}

Це здебільшого працює за призначенням, за винятком випадків, коли він повертається помилковим, хоча два екземпляри структури мають однакові члени (я перевірив налагоджувач eclipse). Після деяких пошуків я виявив, що memcmpможе вийти з ладу через прокладену структуру.

Чи є більш правильний спосіб порівняння пам’яті, байдужої до прокладки? Я не в змозі змінювати використовувані структури (вони є частиною API, який я використовую), і багато різних використовуваних структур мають кілька різних членів, і тому їх не можна порівнювати індивідуально загальним способом (наскільки мені відомо).

Редагувати: Я, на жаль, застряг із C ++ 11. Чи варто було згадати про це раніше ...


чи можете ви показати приклад, коли це не вдається? Прокладка повинна бути однаковою для всіх екземплярів одного типу, ні?
idclev 463035818

1
@ idclev463035818 Padding не визначено, ви не можете припустити, що це значення, і я вважаю, що 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для порівняння), якщо елемент вже не працює ==, і обробляє масиви.

Для цього знадобиться трохи бібліотеки (100ish рядків коду?) Разом із написанням трохи даних про "відображення" вручну на кожного члена. Якщо кількість структур у вас обмежена, можливо, буде простіше написати код структури вручну.


Ймовірно, способи отримати

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

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Ключове слово повинно усунути кілька помилок визначення. Скористайтесь кнопкою [задати питання] після отримання мінімально відтворюваного прикладу
Yakk - Adam Nevraumont

7

Ви маєте рацію, що підкладка перешкоджає порівнянню довільних типів таким чином.

Ви можете вжити заходів:

  • Якщо ви контролюєте, Dataнаприклад, gcc має __attribute__((packed)). Це впливає на продуктивність, але, можливо, варто спробувати. Хоча, я повинен визнати, що я не знаю, чи packedдозволяє вам повністю заборонити прокладку. Gcc doc каже:

Цей атрибут, доданий до визначення типу структура або об'єднання, вказує, що кожен член структури або об'єднання розміщується для мінімізації необхідної пам'яті. Якщо додається до визначення перерахунків, це вказує на те, що слід використовувати найменший інтегральний тип.

  • Якщо ви не контролюєте, Dataто, принаймні, std::has_unique_object_representations<T>можна сказати, чи дасть ваше порівняння правильні результати:

Якщо T є TriviallyCopyable і якщо будь-які два об'єкти типу T з однаковим значенням мають однакове представлення об'єктів, надає постійному значенню члена рівне істинне. Для будь-якого іншого типу значення хибне.

і далі:

Ця ознака була введена для того, щоб можна було визначити, чи можна тип хешувати правильно, хешируючи його об'єктне представлення у вигляді байтового масиву.

PS: Я звертався лише до прокладки, але не забувайте, що типи, які можна порівняти рівними для екземплярів з різним представленням у пам'яті, аж ніяк не рідкісні (наприклад std::string, std::vectorта багато інших).


1
Мені подобається ця відповідь. За допомогою цього типу ознаки ви можете використовувати SFINAE для використання memcmpна конструкціях без прокладки і застосовувати operator==лише за потреби.
Yksisarvinen

Добре, дякую. З цим я сміливо можу зробити висновок, що мені потрібно зробити кілька ручних роздумів.
Фредрік Енеторп

6

Коротше кажучи: не можливо загальним способом.

Проблема memcmpполягає в тому, що прокладка може містити довільні дані, а значить, memcmpможе вийти з ладу. Якби було способом дізнатися, де знаходиться підкладка, ви могли б зняти з нуля ці біти, а потім порівняти представлення даних, що перевірило б рівність, якщо члени тривільно порівнянні (що не так, тобто, std::stringоскільки два рядки можуть містять різні покажчики, але два загострені діаграми є рівними). Але я не знаю жодного способу потрапити на набивання конструкцій. Ви можете спробувати сказати своєму компілятору упакувати структури, але це зробить доступ повільнішим і насправді не гарантовано працювати.

Найчистіший спосіб здійснити це - порівняти всіх членів. Звичайно, це не можливо в загальному вигляді (поки ми не отримаємо компіляції часу та мета-класи в C ++ 23 або новіших версіях). З моменту C ++ 20 можна створити типовий параметр, operator<=>але я думаю, що це було б можливим лише як функція-член, так що це знову ж таки не застосовується. Якщо вам пощастило, і всі структури, які ви хочете порівняти, operator==визначені, ви, звичайно, можете просто скористатися цим. Але це не гарантується.

EDIT: Гаразд, існує справді абсолютно хакітний і дещо загальний спосіб для агрегатів. (Я переписав лише перетворення в кортежі, у них є оператор порівняння за замовчуванням). богболт


Гарний хак! На жаль, я застряг у 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
Хоча це дуже корисна функція, вона не відповідає на запитання. ОП сказав: "Я не в змозі змінити використовувані структури", це означає, що, навіть якщо оператори рівності C ++ 20 були доступні, ОП не зможе їх використовувати, оскільки дефолт ==або <=>оператори можуть бути зроблені лише в межах класу.
Нікол Болас

Як сказав Нікол Болас, я не можу змінити структури.
Фредрік Енеторп

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
Коментари під верхньою відповіддю в цьому посиланні, схоже, вказують на те, що це не так. Сама відповідь говорить лише про те, що прокладки не потрібно копіювати, але не так, щоб вони не були . Я теж точно не знаю.
волоський горіх

Зараз я перевірив це, і він не працює. Призначення не залишає байтів для забивання недоторканими.
Фредрік Енеторп

0

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

За допомогою цієї бібліотеки ми можемо повторити різні поля структури з їх відповідним типом у чисто загально-шаблоновому коді. Антоній використовував це, наприклад, для того, щоб мати змогу передавати довільні структури до вихідного потоку з правильними типами повністю загально. Цілком очевидно, що порівняння також може бути можливим застосуванням цього підходу.

... але вам знадобиться C ++ 14. Принаймні, це краще, ніж C ++ 17 та пізніші пропозиції в інших відповідях :-P

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.