Làm thế nào để so sánh các cấu trúc chung trong C ++?


13

Tôi muốn so sánh các cấu trúc theo cách chung chung và tôi đã làm một cái gì đó như thế này (tôi không thể chia sẻ nguồn thực tế, vì vậy hãy hỏi thêm chi tiết nếu cần thiết):

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

Điều này chủ yếu hoạt động như dự định, ngoại trừ đôi khi nó trả về false mặc dù hai trường hợp cấu trúc có các thành viên giống hệt nhau (tôi đã kiểm tra với trình gỡ lỗi nhật thực). Sau một số tìm kiếm tôi phát hiện ra rằng memcmpcó thể thất bại do cấu trúc được sử dụng được đệm.

Có cách nào phù hợp hơn để so sánh bộ nhớ không quan tâm đến việc đệm không? Tôi không thể sửa đổi các cấu trúc được sử dụng (chúng là một phần của API tôi đang sử dụng) và nhiều cấu trúc khác nhau được sử dụng có một số thành viên khác nhau và do đó không thể so sánh riêng lẻ theo cách chung (theo hiểu biết của tôi).

Chỉnh sửa: Tôi không may bị mắc kẹt với C ++ 11. Nên đã đề cập điều này sớm hơn ...


bạn có thể đưa ra một ví dụ mà điều này thất bại? Đệm phải giống nhau cho tất cả các loại của một loại, không?
idclev 463035818

1
@ idclev463035818 Đệm không được chỉ định, bạn không thể cho rằng đó là giá trị và tôi tin rằng đó là UB để thử đọc nó (không chắc chắn về phần cuối cùng đó).
François Andrieux

@ idclev463035818 Đệm nằm ở cùng một vị trí tương đối trong bộ nhớ nhưng nó có thể có dữ liệu khác nhau. Nó bị loại bỏ trong các sử dụng thông thường của struct để trình biên dịch có thể không bận tâm đến nó.
NO_NAME

2
@ idclev463035818 Phần đệm có cùng kích thước. Trạng thái của các bit cấu thành nên phần đệm có thể là bất cứ thứ gì. Khi bạn memcmpbao gồm các bit đệm trong so sánh của bạn.
François Andrieux

1
Tôi đồng ý với Yksisarvinen ... sử dụng các lớp, không phải cấu trúc và triển ==khai toán tử. Việc sử dụng memcmplà không đáng tin cậy, và sớm hay muộn bạn sẽ phải đối phó với một số lớp phải "làm điều đó khác một chút so với các lớp khác." Nó rất sạch sẽ và hiệu quả để thực hiện điều đó trong một toán tử. Hành vi thực tế sẽ là đa hình nhưng mã nguồn sẽ sạch ... và, rõ ràng.
Mike Robinson

Câu trả lời:


7

Không, memcmpkhông phù hợp để làm điều này. Và sự phản chiếu trong C ++ là không đủ để làm điều này vào thời điểm này (sẽ có các trình biên dịch thử nghiệm hỗ trợ sự phản chiếu đủ mạnh để thực hiện điều này và có thể có các tính năng bạn cần).

Không có phản xạ tích hợp, cách dễ nhất để giải quyết vấn đề của bạn là thực hiện một số phản xạ thủ công.

Thực hiện việc này:

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

chúng tôi muốn thực hiện số lượng công việc tối thiểu để có thể so sánh hai trong số này.

Nếu chúng ta có:

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

hoặc là

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

cho , sau đó:

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

làm một công việc khá tốt.

Chúng ta có thể mở rộng quá trình này để được đệ quy với một chút công việc; thay vì so sánh các mối quan hệ, hãy so sánh từng phần tử được gói trong một mẫu và mẫu đó operator==áp dụng đệ quy quy tắc này (bao bọc phần tử as_tieđể so sánh) trừ khi phần tử đã hoạt động ==và xử lý các mảng.

Điều này sẽ yêu cầu một chút thư viện (100 dòng mã?) Cùng với việc viết một chút dữ liệu "phản chiếu" thủ công cho mỗi thành viên. Nếu số lượng cấu trúc bạn có bị giới hạn, việc viết mã theo cấu trúc theo cách thủ công có thể dễ dàng hơn.


Có lẽ có nhiều cách để có được

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

để tạo as_tiecấu trúc bằng cách sử dụng các macro khủng khiếp. Nhưng as_tieđủ đơn giản. Trong sự lặp lại gây khó chịu; Điều này rất hữu ích:

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

trong tình huống này và nhiều người khác. Với RETURNS, viết as_tielà:

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

loại bỏ sự lặp lại.


Đây là một cú đâm làm cho nó đệ quy:

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 (mảng) (đệ quy đầy đủ, thậm chí hỗ trợ các mảng của mảng):

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>{} ) )

Ví dụ sống .

Ở đây tôi sử dụng một std::arraysố refl_tie. Tốc độ này nhanh hơn nhiều so với bộ Refl_tie trước đây của tôi tại thời điểm biên dịch.

Cũng thế

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

sử dụng std::crefở đây thay vì std::tiecó thể tiết kiệm chi phí thời gian biên dịch, vì đây creflà một lớp đơn giản hơn nhiều so với tuple.

Cuối cùng, bạn nên thêm

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

điều này sẽ ngăn các thành viên mảng phân rã thành các con trỏ và quay trở lại sự bình đẳng con trỏ (mà bạn có thể không muốn từ các mảng).

Nếu không có điều này, nếu bạn truyền một mảng cho một cấu trúc không được phản ánh, nó sẽ rơi vào cấu trúc con trỏ đến không phản xạ refl_tie, hoạt động và trả về vô nghĩa.

Với điều này, bạn kết thúc với một lỗi thời gian biên dịch.


Hỗ trợ đệ quy thông qua các loại thư viện là khó khăn. Bạn có thể std::tiehọ:

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

nhưng điều đó không hỗ trợ đệ quy thông qua nó.


Tôi muốn theo đuổi loại giải pháp này với các phản xạ thủ công. Mã bạn cung cấp dường như không hoạt động với C ++ 11. Bất cứ cơ hội nào bạn có thể giúp tôi với điều đó?
Fredrik Enetorp

1
Lý do điều này không hoạt động trong C ++ 11 là do không có kiểu trả về theo dõi trên as_tie. Bắt đầu từ C ++ 14, điều này được suy luận tự động. Bạn có thể sử dụng auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));trong C ++ 11. Hoặc nêu rõ loại trả về.
Darhuuk

1
@FredrikEnetorp Đã sửa, cộng với một macro giúp dễ viết. Công việc để làm cho nó hoạt động đệ quy đầy đủ (vì vậy một cấu trúc cấu trúc, trong đó các cấu trúc con có as_tiehỗ trợ, tự động hoạt động) và các thành viên mảng hỗ trợ không chi tiết, nhưng có thể.
Yakk - Adam Nevraumont

Cảm ơn bạn. Tôi đã làm các macro khủng khiếp hơi khác nhau, nhưng chức năng tương đương. Chỉ một vấn đề nữa. Tôi đang cố gắng khái quát hóa sự so sánh trong một tệp tiêu đề riêng biệt và đưa nó vào các tệp thử nghiệm gmock khác nhau. Điều này dẫn đến thông báo lỗi: nhiều định nghĩa của `as_tie (Test1 const &) 'Tôi đang cố gắng nội tuyến chúng nhưng không thể làm cho nó hoạt động.
Fredrik Enetorp

1
@FredrikEnetorp inlineTừ khóa sẽ khiến nhiều lỗi định nghĩa biến mất. Sử dụng nút [đặt câu hỏi] sau khi bạn lấy một ví dụ có thể lặp lại tối thiểu
Yakk - Adam Nevraumont

7

Bạn đã đúng rằng việc đệm được theo cách của bạn để so sánh các loại tùy ý theo cách này.

Có những biện pháp bạn có thể thực hiện:

  • Nếu bạn đang kiểm soát Datathì gcc có __attribute__((packed)). Nó có tác động đến hiệu suất, nhưng nó có thể đáng để thử. Mặc dù vậy, tôi phải thừa nhận rằng tôi không biết liệu có packedcho phép bạn không cho phép đệm hoàn toàn hay không. Gcc doc nói:

Thuộc tính này, được gắn với định nghĩa kiểu cấu trúc hoặc kết hợp, chỉ định rằng mỗi thành viên của cấu trúc hoặc liên kết được đặt để giảm thiểu bộ nhớ cần thiết. Khi được gắn với một định nghĩa enum, nó chỉ ra rằng loại tích phân nhỏ nhất nên được sử dụng.

Nếu T là TrivivelyCopyable và nếu bất kỳ hai đối tượng loại T có cùng giá trị có cùng biểu diễn đối tượng, cung cấp giá trị hằng số thành viên bằng đúng. Đối với bất kỳ loại nào khác, giá trị là sai.

và xa hơn:

Đặc điểm này đã được giới thiệu để có thể xác định xem một loại có thể được băm chính xác hay không bằng cách băm biểu diễn đối tượng của nó dưới dạng một mảng byte.

PS: Tôi chỉ giải quyết phần đệm, nhưng đừng quên rằng các loại có thể so sánh bằng nhau cho các thể hiện với biểu diễn khác nhau trong bộ nhớ không có nghĩa là hiếm (ví dụ std::string, std::vectorvà nhiều loại khác).


1
Tôi thích câu trả lời này. Với đặc điểm loại này, bạn có thể sử dụng SFINAE để sử dụng memcmptrên các cấu trúc không có phần đệm và operator==chỉ thực hiện khi cần thiết.
Yksisarvinen

Được rồi cảm ơn. Với điều này tôi có thể kết luận một cách an toàn rằng tôi cần phải thực hiện một số phản xạ thủ công.
Fredrik Enetorp

6

Tóm lại: Không thể một cách chung chung.

Vấn đề với memcmplà phần đệm có thể chứa dữ liệu tùy ý và do đó memcmpcó thể thất bại. Nếu có một cách để tìm ra vị trí của phần đệm, bạn có thể loại bỏ các bit đó và sau đó so sánh các biểu diễn dữ liệu, điều đó sẽ kiểm tra sự bằng nhau nếu các thành viên có thể so sánh một cách tầm thường (đó không phải là trường hợp std::stringvì hai chuỗi có thể chứa các con trỏ khác nhau, nhưng hai mảng char nhọn bằng nhau). Nhưng tôi biết không có cách nào để có được phần đệm của các cấu trúc. Bạn có thể cố gắng yêu cầu trình biên dịch của mình đóng gói các cấu trúc, nhưng điều này sẽ làm cho việc truy cập chậm hơn và không thực sự đảm bảo để hoạt động.

Cách sạch nhất để thực hiện điều này là so sánh tất cả các thành viên. Tất nhiên điều này không thực sự có thể theo một cách chung chung (cho đến khi chúng ta có được các phản xạ thời gian biên dịch và các lớp meta trong C ++ 23 trở lên). Từ C ++ 20 trở đi, người ta có thể tạo mặc định operator<=>nhưng tôi nghĩ điều này cũng chỉ có thể là một hàm thành viên, vì vậy, điều này một lần nữa không thực sự áp dụng được. Nếu bạn may mắn và tất cả các cấu trúc bạn muốn so sánh có một operator==định nghĩa, tất nhiên bạn có thể chỉ cần sử dụng nó. Nhưng điều đó không được đảm bảo.

EDIT: Ok, thực sự có một cách hoàn toàn hacky và hơi chung chung cho các tập hợp. (Tôi chỉ viết chuyển đổi sang bộ dữ liệu, những người có toán tử so sánh mặc định). đỡ đầu


Đẹp hack! Thật không may, tôi bị mắc kẹt với C ++ 11 vì vậy tôi không thể sử dụng nó.
Fredrik Enetorp

2

C ++ 20 hỗ trợ comaparisons mặc định

#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
Mặc dù đó là một tính năng rất hữu ích, nhưng nó không trả lời câu hỏi như đã hỏi. OP đã nói "Tôi không thể thay đổi cấu trúc sử dụng", có nghĩa là, ngay cả khi C ++ nhà khai thác bình đẳng 20 mặc định đã có sẵn, OP sẽ không thể sử dụng chúng kể từ mặc định là ==hoặc <=>nhà khai thác chỉ có thể được thực hiện ở phạm vi lớp học.
Nicol Bolas

Giống như Nicol Bolas đã nói, tôi không thể sửa đổi các cấu trúc.
Fredrik Enetorp

1

Giả sử dữ liệu POD, toán tử gán mặc định chỉ sao chép các byte thành viên. (thực sự không chắc chắn 100% về điều đó, đừng hiểu ý tôi)

Bạn có thể sử dụng điều này để lợi thế của bạn:

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 Bạn nói đúng đó là một câu trả lời khủng khiếp. Viết lại một cái.
Kostas

Liệu tiêu chuẩn có đảm bảo rằng phép gán để lại các byte đệm không bị ảnh hưởng? Vẫn còn một mối quan tâm về việc biểu diễn nhiều đối tượng cho cùng một giá trị trong các loại cơ bản.
quả óc chó

@walnut Tôi tin là có .
Kostas

1
Các ý kiến ​​dưới câu trả lời hàng đầu trong liên kết đó dường như chỉ ra rằng nó không. Câu trả lời chỉ nói rằng phần đệm không cần phải sao chép, nhưng không phải là không có . Tôi cũng không biết chắc chắn.
quả óc chó

Bây giờ tôi đã thử nó và nó không hoạt động. Bài tập không để lại các byte đệm không bị ảnh hưởng.
Fredrik Enetorp

0

Tôi tin rằng bạn có thể có thể đưa ra một giải pháp cho voodoo cực kỳ quỷ quyệt của Antony Polukhin trong magic_getthư viện - cho các cấu trúc, không phải cho các lớp phức tạp.

Với thư viện đó, chúng tôi có thể lặp lại các trường khác nhau của một cấu trúc, với loại thích hợp của chúng, trong mã hoàn toàn chung chung. Antony đã sử dụng điều này, ví dụ, để có thể truyền các cấu trúc tùy ý đến một luồng đầu ra với các loại chính xác, hoàn toàn tổng quát. Lý do là so sánh cũng có thể là một ứng dụng khả thi của phương pháp này.

... nhưng bạn sẽ cần C ++ 14. Ít nhất là nó tốt hơn C ++ 17 và các đề xuất sau này trong các câu trả lời khác :-P

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.