วิธีเปรียบเทียบ struct ทั่วไปใน C ++?


13

ฉันต้องการเปรียบเทียบ structs ด้วยวิธีทั่วไปและฉันทำบางสิ่งเช่นนี้ (ฉันไม่สามารถแบ่งปันแหล่งที่มาจริงได้ดังนั้นขอรายละเอียดเพิ่มเติมหากจำเป็น):

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;
}

ส่วนใหญ่ใช้งานได้ตามที่ตั้งใจยกเว้นบางครั้งมันจะคืนค่าเท็จแม้ว่าอินสแตนซ์ของ struct สองรายการมีสมาชิกเหมือนกัน หลังจากค้นหาบางฉันพบว่าmemcmpสามารถล้มเหลวเนื่องจากโครงสร้างที่ใช้เป็นเบาะ

มีวิธีที่เหมาะสมกว่าในการเปรียบเทียบหน่วยความจำที่ไม่แยแสกับการขยายหรือไม่ ฉันไม่สามารถแก้ไข structs ที่ใช้ (พวกเขาเป็นส่วนหนึ่งของ API ที่ฉันใช้) และ structs ที่แตกต่างกันจำนวนมากที่ใช้มีสมาชิกที่แตกต่างกันและดังนั้นจึงไม่สามารถเปรียบเทียบเป็นรายบุคคลในแบบทั่วไป (กับความรู้ของฉัน)

แก้ไข: ฉันโชคไม่ดีติดอยู่กับ C ++ 11 ควรพูดถึงเรื่องนี้ก่อนหน้านี้ ...


คุณสามารถแสดงตัวอย่างที่สิ่งนี้ล้มเหลวได้หรือไม่ การเติมเต็มควรเหมือนกันสำหรับอินสแตนซ์ทั้งหมดของประเภทเดียวหรือไม่?
idclev 463035818

1
@ idclev463035818 การแพ็ดดิ้งไม่ได้ระบุคุณไม่สามารถถือว่ามันคุ้มค่าและฉันเชื่อว่ามันเป็น UB ที่จะลองอ่านมัน (ไม่แน่ใจในส่วนสุดท้าย)
François Andrieux

@ idclev463035818 การแพ็ดดิ้งอยู่ในตำแหน่งเดียวกันในหน่วยความจำ แต่สามารถมีข้อมูลที่แตกต่างกัน มันถูกทิ้งในการใช้งานปกติของ struct ดังนั้นคอมไพเลอร์อาจไม่สนใจที่จะเป็นศูนย์
NO_NAME

2
@ idclev463035818 ช่องว่างภายในมีขนาดเท่ากัน สถานะของบิตที่ประกอบเป็นช่องว่างภายในสามารถเป็นอะไรก็ได้ เมื่อคุณmemcmpรวมบิตเสริมเหล่านั้นในการเปรียบเทียบของคุณ
François Andrieux

1
ฉันเห็นด้วยกับ Yksisarvinen ... ใช้คลาสไม่ใช่ structs และใช้==โอเปอเรเตอร์ การใช้memcmpไม่น่าเชื่อถือและไม่ช้าก็เร็วคุณจะต้องจัดการกับคลาสที่ต้อง "ทำแตกต่างจากคนอื่นเล็กน้อย" มันสะอาดและมีประสิทธิภาพมากในการติดตั้งในตัวดำเนินการ พฤติกรรมที่แท้จริงจะเป็น polymorphic แต่ซอร์สโค้ดจะสะอาด ... และชัดเจน
Mike Robinson

คำตอบ:


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 line?) พร้อมกับการเขียนข้อมูล "reflect" ของสมาชิกต่อสมาชิกเล็กน้อย หากจำนวนของ structs ที่คุณมี จำกัด มันอาจจะง่ายต่อการเขียนรหัสต่อโครงสร้างด้วยตนเอง


อาจมีวิธีที่จะได้รับ

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นี้เร็วกว่า tuple ก่อนหน้าของฉันของ 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 มีโอกาสใดที่คุณสามารถช่วยฉันได้
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รับการสนับสนุนทำงานโดยอัตโนมัติ) และสมาชิกอาร์เรย์สนับสนุนไม่ละเอียด แต่ก็เป็นไปได้
Yakk - Adam Nevraumont

ขอบคุณ. ฉันทำมาโครที่น่ากลัวแตกต่างกันเล็กน้อย แต่มีประโยชน์เทียบเท่า อีกหนึ่งปัญหา ฉันพยายามสรุปการเปรียบเทียบในไฟล์ส่วนหัวแยกและรวมไว้ในไฟล์ทดสอบ gmock ต่างๆ สิ่งนี้ส่งผลให้เกิดข้อความแสดงข้อผิดพลาด: คำจำกัดความหลายคำของ `as_tie (Test1 const &) 'ฉันพยายามอินไลน์ แต่ไม่สามารถทำงานได้
Fredrik Enetorp

1
@FredrikEnetorp inlineคำหลักควรทำให้ข้อผิดพลาดหลาย ๆ คำจำกัดความหายไป ใช้ปุ่ม [ถามคำถาม] หลังจากคุณได้รับตัวอย่างที่ทำซ้ำ
Yakk - Adam Nevraumont

7

คุณถูกต้องที่ padding ได้รับในทางของคุณเปรียบเทียบประเภทโดยพลการในลักษณะนี้

มีมาตรการที่คุณสามารถทำได้:

  • ถ้าคุณอยู่ในการควบคุมของDataแล้วเช่น GCC __attribute__((packed))มี มันส่งผลกระทบต่อประสิทธิภาพการทำงาน แต่อาจคุ้มค่าที่จะลองดู แม้ว่าฉันต้องยอมรับว่าฉันไม่รู้ว่าpackedจะช่วยให้คุณไม่อนุญาตให้ช่องว่างภายในสมบูรณ์หรือไม่ Gcc docพูดว่า:

แอ็ตทริบิวต์นี้ที่แนบกับ struct หรือนิยามชนิดยูเนี่ยนระบุว่าสมาชิกของโครงสร้างหรือยูเนี่ยนแต่ละคนจะถูกวางไว้เพื่อลดหน่วยความจำที่จำเป็น เมื่อเชื่อมต่อกับนิยาม enum แสดงว่าควรใช้ชนิดอินทิกรัลที่เล็กที่สุด

  • หากคุณไม่สามารถควบคุมDataได้อย่างน้อยก็std::has_unique_object_representations<T>สามารถบอกคุณได้ว่าการเปรียบเทียบของคุณจะให้ผลลัพธ์ที่ถูกต้องหรือไม่:

ถ้า T คือ TriviallyCopyable และถ้าวัตถุสองชนิดที่มีค่าเดียวกันมีการแทนวัตถุเดียวกันให้ค่าคงที่สมาชิกเท่ากับจริง สำหรับประเภทอื่น ๆ ค่าเป็นเท็จ

และต่อไป:

ลักษณะนี้ถูกนำมาใช้เพื่อให้เป็นไปได้ที่จะตรวจสอบว่าชนิดสามารถถูกแฮชอย่างถูกต้องโดย hashing การแสดงวัตถุของมันเป็นอาร์เรย์ไบต์

PS: ผมจ่าหน้าเพียง padding แต่ไม่ลืมว่าประเภทที่สามารถเปรียบเทียบที่เท่าเทียมกันสำหรับกรณีที่มีการแสดงที่แตกต่างกันในความทรงจำโดยไม่มีหมายถึงหายาก (เช่นstd::string, std::vectorและอื่น ๆ อีกมากมาย)


1
ฉันชอบคำตอบนี้ ด้วยคุณสมบัติประเภทนี้คุณสามารถใช้ SFINAE เพื่อใช้งานmemcmpบน structs โดยไม่มีการขยายและใช้operator==เมื่อจำเป็นเท่านั้น
Yksisarvinen

โอเคขอบคุณ. ด้วยสิ่งนี้ฉันสามารถสรุปได้อย่างปลอดภัยว่าฉันต้องทำการสะท้อนด้วยตนเอง
Fredrik Enetorp

6

กล่าวโดยย่อ: เป็นไปไม่ได้ในวิธีทั่วไป

ปัญหาmemcmpคือว่าการขยายอาจมีข้อมูลโดยพลการและmemcmpอาจทำให้ล้มเหลว หากมีวิธีหาว่าช่องว่างอยู่ที่ไหนคุณสามารถ zero-out บิตเหล่านั้นแล้วเปรียบเทียบ data Representations ซึ่งจะตรวจสอบความเท่าเทียมกันหากสมาชิกเปรียบเทียบกันเล็กน้อย (ซึ่งไม่ใช่กรณีเช่นstd::stringเนื่องจากสองสายสามารถ มีพอยน์เตอร์ที่แตกต่างกัน แต่ char-arrays ที่แหลมสองอันนั้นเท่ากัน) แต่ฉันรู้ว่าไม่มีทางที่จะได้รับการซ้อนของ structs คุณสามารถลองบอกคอมไพเลอร์ของคุณเพื่อแพ็ค structs แต่สิ่งนี้จะทำให้การเข้าถึงช้าลงและไม่ได้รับประกันว่าจะได้ผลจริง ๆ

วิธีที่สะอาดที่สุดในการดำเนินการนี้คือการเปรียบเทียบสมาชิกทั้งหมด แน่นอนว่านี่เป็นไปไม่ได้จริงๆในวิธีทั่วไป (จนกว่าเราจะได้รับการสะท้อนเวลารวบรวมและคลาสเมตาใน C ++ 23 หรือใหม่กว่า) ตั้งแต่ C ++ 20 เป็นต้นไปเราสามารถสร้างค่าเริ่มต้นได้operator<=>แต่ฉันคิดว่านี่จะเป็นไปได้ในฐานะฟังก์ชันสมาชิกเท่านั้นดังนั้นจึงไม่สามารถใช้งานได้อีก หากคุณโชคดีและโครงสร้างทั้งหมดที่คุณต้องการเปรียบเทียบมีการoperator==กำหนดไว้แน่นอนคุณสามารถใช้มันได้ แต่นั่นไม่รับประกัน

แก้ไข:ตกลงมีจริงแฮ็คทั้งหมดและค่อนข้างทั่วไปวิธีการรวม (ฉันเขียนเฉพาะการแปลงเป็นสิ่งอันดับพวกเขามีตัวดำเนินการเปรียบเทียบเริ่มต้น) godbolt


แฮ็คที่ดี! น่าเสียดายที่ฉันติดอยู่กับ C ++ 11 ดังนั้นฉันจึงไม่สามารถใช้งานได้
Fredrik Enetorp

2

C ++ 20 รองรับcomaparisons เริ่มต้น

#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 กล่าวว่า "ฉันไม่สามารถแก้ไข structs ที่ใช้" ซึ่งหมายความว่าแม้ว่าผู้ประกอบการที่มีความเท่าเทียมกันเริ่มต้นของ C ++ 20 จะพร้อมใช้งาน OP จะไม่สามารถใช้งานได้เนื่องจากการเริ่มต้น==หรือ<=>ตัวดำเนินการสามารถทำได้ ที่ขอบเขตของคลาส
Nicol Bolas

เช่นเดียวกับ Nicol Bolas กล่าวว่าฉันไม่สามารถแก้ไข structs ได้
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; 
}

@ วอลนัตคุณพูดถูกนั่นเป็นคำตอบที่แย่มาก เขียนใหม่
Kostas

มาตรฐานรับประกันว่าการมอบหมายนั้นปล่อยให้ไม่มีการแตะต้องไบต์หรือไม่? นอกจากนี้ยังมีข้อกังวลเกี่ยวกับการแสดงวัตถุหลายรายการสำหรับค่าเดียวกันในประเภทพื้นฐาน
วอลนัท

@walnut ผมเชื่อว่ามันจะ
Kostas

1
ความคิดเห็นภายใต้คำตอบที่ดีที่สุดในลิงค์นั้นดูเหมือนว่าไม่มี คำตอบของตัวเองเพียงคนเดียวที่บอกว่าช่องว่างภายในไม่จำเป็นต้องถูกคัดลอก แต่ไม่ว่ามันmusn't แต่ฉันก็ไม่รู้เหมือนกัน
วอลนัท

ฉันได้ทำการทดสอบแล้วและใช้งานไม่ได้ การมอบหมายไม่ปล่อยให้ไบต์ padding ถูกแตะต้อง
Fredrik Enetorp

0

ฉันเชื่อว่าคุณอาจสามารถแก้ปัญหาเกี่ยวกับลัทธิวูดูอันน่าพิศวงของ Antony Polukhin ในmagic_getห้องสมุดได้ - สำหรับ structs ไม่ใช่คลาสที่ซับซ้อน

ด้วยไลบรารีนั้นเราสามารถทำซ้ำฟิลด์ที่แตกต่างของโครงสร้างด้วยชนิดที่เหมาะสมในโค้ดทั่วไปอย่าง - templated ตัวอย่างเช่นแอนโทนีใช้สิ่งนี้เพื่อให้สามารถสตรีม struct โดยพลการไปยังเอาต์พุตสตรีมด้วยประเภทที่ถูกต้องได้อย่างสมบูรณ์โดยทั่วไป มีเหตุผลที่การเปรียบเทียบอาจเป็นไปได้ที่จะใช้วิธีนี้

... แต่คุณต้องการ C ++ 14 อย่างน้อยก็ดีกว่า C ++ 17 และคำแนะนำในภายหลังในคำตอบอื่น ๆ :-P

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.