Это безопасный способ реализации универсального оператора == и оператора <? - PullRequest
11 голосов
/ 31 августа 2010

После просмотра этого вопроса я впервые подумал, что было бы тривиально определить общие эквивалентности и операторы отношений:

#include <cstring>

template<class T>
bool operator==(const T& a, const T& b) {

    return std::memcmp(&a, &b, sizeof(T)) == 0;

}

template<class T>
bool operator<(const T& a, const T& b) {

    return std::memcmp(&a, &b, sizeof(T)) < 0;

}

using namespace std::rel_ops станет еще более полезным, так как он будет сделан полностью универсальным в стандартных реализациях операторов == и <. Очевидно, что это не выполняет сравнение по элементам, а поразрядное, как будто тип содержит только члены POD. Это не совсем соответствует тому, как C ++ генерирует конструкторы копирования, например, которые делают выполняют копирование по элементам.

Но мне интересно, действительно ли приведенная выше реализация действительно безопасна. Структуры, естественно, будут иметь одинаковую упаковку одинакового типа, но гарантированно ли содержимое наполнителя будет одинаковым (например, заполнено нулями)? Есть ли причины или ситуации, в которых это не сработает?

Ответы [ 6 ]

13 голосов
/ 31 августа 2010

Нет - просто например, если у вас есть T == (float | double | long double), ваш operator== не будет работать правильно. Два NaN никогда не должны сравниваться как равные, даже если они имеют одинаковую битовую комбинацию (на самом деле, один из распространенных методов обнаружения NaN - это сравнение числа с самим собой - если оно не равно самому себе, это NaN). Аналогично, два числа с плавающей запятой со всеми битами в их показателях степени, установленными в 0, имеют значение 0,0 (точно) независимо от того, какие биты могут быть установлены / сброшены в значении.

Ваш operator< имеет еще меньше шансов на правильную работу. Например, рассмотрим типичную реализацию std::string, которая выглядит примерно так:

template <class charT>
class string { 
    charT *data;
    size_t length;
    size_t buffer_size;
public:
    // ...
};

При таком упорядочении членов ваш operator< выполнит сравнение на основе адресов буферов, в которых строки хранят свои данные. Если, к примеру, он был записан сначала с элементом length, то при сравнении вы будете использовать длины строк в качестве первичных ключей. В любом случае, он не будет делать сравнение, основанное на фактическом содержимом строки, потому что он будет когда-либо смотреть только на значение указателя data, а не на то, на что он указывает, на что вы очень хочу / нужен.

Редактировать: Что касается заполнения, нет требования, чтобы содержимое заполнения было одинаковым. Теоретически также возможно, чтобы заполнение представляло собой какое-то представление ловушки, которое вызовет сигнал, сгенерирует исключение или что-то в этом порядке, если вы даже попытаетесь посмотреть на него вообще. Чтобы избежать такого представления ловушек, вам нужно использовать что-то вроде приведения, чтобы рассматривать его как буфер unsigned char s. memcmp может сделать это, но с другой стороны, это не может ...

Также обратите внимание, что одинаковые типы объектов не обязательно означают использование одинакового выравнивания элементов. Это распространенный метод реализации, но для компилятора также вполне возможно сделать что-то вроде использования различных выравниваний в зависимости от того, как часто он «думает», что будет использоваться конкретный объект, и включить какой-либо тег в * 1023. * объект (например, значение, записанное в первый байт заполнения), который сообщает выравнивание для этого конкретного экземпляра. Аналогично, он может разделять объекты по (например) адресу, поэтому объект, расположенный по четному адресу, имеет 2-байтовое выравнивание, по адресу, кратному четырем, имеет 4-байтовое выравнивание и т. Д. (Это не может быть используется для типов POD, но в противном случае все ставки отключены).

Ничего из этого не является вероятным или распространенным, но в стандарте я не могу придумать ничего такого, что бы запрещало их.

3 голосов
/ 31 августа 2010

Даже для POD оператор == может быть неправильным. Это связано с выравниванием структур, подобных следующей, которая занимает 8 байт в моем компиляторе.

class Foo {
  char foo; /// three bytes between foo and bar
  int bar;
};
2 голосов
/ 31 августа 2010

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

Однажды тебя укусят.

0 голосов
/ 31 августа 2010

Любая структура или класс, содержащий один указатель, мгновенно потерпит неудачу при любом значимом сравнении.Эти операторы будут работать ТОЛЬКО для любого класса, который представляет собой Plain Old Data или POD.Другой отвечающий правильно указал на плавающие точки как случай, когда даже это не будет иметь значение true, и на байты заполнения.

Краткий ответ: Если бы это была умная идея, язык имел бы ее как конструкторы / назначения копирования по умолчаниюоператоры.

0 голосов
/ 31 августа 2010

Многое может зависеть от вашего определения эквивалентности.

например. если какие-либо члены, которые вы сравниваете в своих классах, являются числами с плавающей запятой.

Вышеприведенная реализация может рассматривать два двойных числа как не равные, даже если они получены из одного и того же математического вычисления с одинаковыми входными данными - поскольку они, возможно, не генерировали точно такой же выходной сигнал - скорее, два очень похожих числа.

Как правило, такие числа должны сравниваться численно с соответствующим допуском.

...