Двойное отрицание в C ++

124

Я только что пришел в проект с довольно большой кодовой базой.

Я в основном имею дело с C ++, и большая часть кода, который они пишут, использует двойное отрицание для своей логической логики.

 if (!!variable && (!!api.lookup("some-string"))) {
       do_some_stuff();
 }                                   

Я знаю, что эти ребята умные программисты, очевидно, что они делают это не случайно.

Я не опытный эксперт по C ++, я только догадываюсь, почему они это делают, так это то, что они хотят сделать абсолютно уверенным, что оцениваемое значение является фактическим логическим представлением. Таким образом, они отрицают это, а затем снова отрицают, чтобы вернуть его фактическое логическое значение.

Это правильно, или я что-то упускаю?

Брайан Джанфоркаро
источник
4
проверьте здесь, уже спрашивали, Есть !! безопасный способ конвертировать в bool в C ++?
Озгюр
Эта тема обсуждалась здесь .
Дима

Ответы:

121

Это уловка для преобразования в логическое значение.

Дон Нойфельд
источник
19
Я думаю, что явно указывать его с помощью (bool) было бы яснее, зачем использовать это хитрое !!, потому что он меньше печатает?
Байян Хуанг,
27
Однако это бессмысленно в C ++ или современном C, или где результат используется только в логическом выражении (как в вопросе). Это было полезно, когда у нас не было boolтипа, чтобы избежать хранения значений, отличных от логических переменных 1и 0в них.
Майк Сеймур
6
@lzprgmr: явное приведение приводит к "предупреждению о производительности" на MSVC. Используя !!или !=0решая проблему, и среди двух я нахожу первый очиститель (поскольку он будет работать с большим количеством типов). Также я согласен с тем, что нет причин использовать их в рассматриваемом коде.
Яков Галка
6
@Noldorin, я думаю, это улучшает читабельность - если вы знаете, что это значит, это просто, аккуратно и логично.
jwg 04
19
Улучшает? Черт возьми ... дай мне немного того, что ты куришь.
Noldorin
73

На самом деле это очень полезная идиома в некоторых контекстах. Возьмите эти макросы (пример из ядра Linux). Для GCC они реализованы следующим образом:

#define likely(cond)   (__builtin_expect(!!(cond), 1))
#define unlikely(cond) (__builtin_expect(!!(cond), 0))

Зачем им это нужно? GCC __builtin_expectобрабатывает свои параметры как longи нет bool, поэтому должна быть какая-то форма преобразования. Поскольку они не знают, что condименно, когда они пишут эти макросы, наиболее общим является использование этой !!идиомы.

Вероятно, они могли бы сделать то же самое, сравнивая с 0, но, на мой взгляд, на самом деле сделать двойное отрицание проще, поскольку это ближе всего к преобразованию в логическое значение, которое есть у C.

Этот код можно использовать и в C ++ ... это вещь с наименьшим общим знаменателем. Если возможно, делайте то, что работает как в C, так и в C ++.

Том Барта
источник
Я думаю, что это имеет большой смысл, если подумать. Я не читал все ответы, но похоже, что процесс преобразования не указан. Если у нас есть значение с высотой 2 бита и значение, имеющее только один бит, у нас будет ненулевое значение. Отрицание ненулевого значения приводит к логическому преобразованию (false, если оно равно нулю, и true в противном случае). Затем снова отрицание приводит к логическому значению, которое представляет исходную истину.
Джои Карсон,
Поскольку SO не позволяет мне обновлять мой комментарий, я исправлю свою ошибку. Отрицание целого значения приводит к логическому преобразованию (false, если оно не равно нулю, в противном случае - true).
Джои Карсон,
51

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

Fizzer
источник
14
Visual C ++ в некоторых случаях дает снижение производительности без этой уловки.
Кирилл Васильевич Лядвинский
1
Я полагаю, было бы лучше просто отключить предупреждение, чем обойти бесполезные предупреждения в коде.
Руслан
Возможно, они этого не осознают. Это действительно имеет смысл в контексте макроса, когда вы можете работать с интегральными типами данных, о которых вы не знаете. Рассмотрим объекты с оператором круглых скобок, перегруженным для возврата целочисленного значения, представляющего битовое поле.
Джои Карсон,
12

Это метод, позволяющий избежать записи (переменная! = 0), т. Е. Преобразования любого типа в логическое значение.

Подобному коду IMO нет места в системах, которые необходимо поддерживать - потому что это не сразу читаемый код (отсюда и вопрос в первую очередь).

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

Ричард Харрисон
источник
8
Мое определение уловки - это то, что не все могут понять при первом чтении. То, что нужно выяснить, - это уловка. Также ужасно, потому что! оператор мог быть перегружен ...
Ричард Харрисон
6
@ orlandu63: простое приведение типов bool(expr): оно действует правильно, и все понимают его намерения с первого взгляда. !!(expr)это двойное отрицание, которое случайно преобразуется в bool ... это непросто.
Адриан Плиссон
12

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

jwfearn
источник
9

Это обходит стороной предупреждение компилятора. Попробуй это:

int _tmain(int argc, _TCHAR* argv[])
{
    int foo = 5;
    bool bar = foo;
    bool baz = !!foo;
    return 0;
}

Строка 'bar' генерирует «принудительное преобразование значения в bool 'true' или 'false' (предупреждение о производительности)» в MSVC ++, но строка 'baz' проходит нормально.

RobH
источник
1
Чаще всего встречается в самом Windows API, который не знает о boolтипе - все кодируется как 0или 1в int.
Марк Рэнсом
4

Оператор! перегружен?
В противном случае они, вероятно, делают это, чтобы преобразовать переменную в логическое значение без предупреждения. Это определенно нестандартный способ ведения дел.

Marcin
источник
4

Разработчики Наследство C не имели логический тип, поэтому они часто #define TRUE 1и #define FALSE 0затем используются произвольные числовые типы данных для булевых сравнений. Теперь, когда у нас есть bool, многие компиляторы будут выдавать предупреждения, когда определенные типы назначений и сравнений выполняются с использованием смеси числовых типов и логических типов. Эти два использования в конечном итоге столкнутся при работе с устаревшим кодом.

Чтобы обойти эту проблему, некоторые разработчики используют следующий логический идентификатор: !num_valueвозвращает bool trueif num_value == 0; falseв противном случае. !!num_valueвозвращает, bool falseесли num_value == 0; trueв противном случае. Одного отрицания достаточно, чтобы преобразовать num_valueв bool; однако двойное отрицание необходимо для восстановления первоначального смысла булевого выражения.

Этот шаблон известен как идиома , т. Е. Что- то, что обычно используется людьми, знакомыми с языком. Поэтому я не вижу в этом антипаттерна, в большей степени, чем хотелось бы static_cast<bool>(num_value). Приведение вполне может дать правильные результаты, но некоторые компиляторы затем выдают предупреждение о производительности, поэтому вам все равно нужно это исправить.

Другой способ решения этой проблемы есть (num_value != FALSE). Я тоже согласен с этим, но в целом !!num_valueон гораздо менее подробен, может быть более ясным и не сбивает с толку, когда вы видите это во второй раз.

KarlU
источник
2

!! использовался для работы с исходным C ++, у которого не было логического типа (как и C).


Пример проблемы:

Внутри if(condition), на conditionпотребности оценить в какой - то тип , как double, int, void*и т.д., но не boolтак как она еще не существует.

Скажем, существует класс int256(256-битное целое число) и все целочисленные преобразования / преобразования были перегружены.

int256 x = foo();
if (x) ...

Чтобы проверить, было ли оно x«истинным» или ненулевым, нужно if (x)преобразовать xв некоторое целое число, а затем оценить, intбыло ли оно ненулевым. Типичная перегрузка (int) xвозвращает только LS биты x. if (x)тогда только тестировал LS биты x.

Но у C ++ есть !оператор. Перегрузка !xобычно оценивает все биты x. Итак, чтобы вернуться к неинвертированной логике if (!!x).

Ref Использовали ли старые версии C ++ оператор класса int при вычислении условия в операторе if ()?

chux - восстановить Монику
источник
1

Как упоминал Марчин , это может иметь значение, если перегрузка оператора происходит. В противном случае в C / C ++ это не имеет значения, кроме случаев, когда вы выполняете одно из следующих действий:

  • прямое сравнение true(или в C что-то вроде TRUEмакроса), что почти всегда плохая идея. Например:

    if (api.lookup("some-string") == true) {...}

  • вам просто нужно что-то преобразовать в строгое значение 0/1. В C ++ присвоение a boolбудет делать это неявно (для тех вещей, которые неявно конвертируются в bool). В C или если вы имеете дело с переменной, отличной от bool, я видел эту идиому, но я предпочитаю ее (some_variable != 0)разнообразие.

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

Майкл Берр
источник
1

Если переменная имеет объектный тип, она может иметь! оператор определен, но не приведен к типу bool (или, что еще хуже, неявно приведен к типу int с другой семантикой. Двойной вызов оператора! приводит к преобразованию в тип bool, которое работает даже в странных случаях.

Джошуа
источник
0

Это правильно, но в C здесь бессмысленно - 'if' и '&&' будут обрабатывать выражение таким же образом без '!!'.

Я полагаю, что причина сделать это в C ++ в том, что '&&' может быть перегружен. Но тогда тоже может '!', Поэтому на самом деле это не гарантирует, что вы получите bool, не глядя на код для типов variableи api.call. Может быть, кто-нибудь с большим опытом C ++ сможет объяснить; возможно, это была мера всесторонней защиты, а не гарантия.

Дариус Бэкон
источник
Компилятор будет обрабатывать значения одинаково в любом случае, если он используется только как операнд для ifили &&, но использование !!может помочь в некоторых компиляторах, если оно if (!!(number & mask))заменено на bit triggered = !!(number & mask); if (triggered); на некоторых встроенных компиляторах с битовыми типами присвоение, например, 256 битовому типу даст ноль. Без него !!кажущееся безопасное преобразование (копирование ifусловия в переменную и последующее ветвление) не будет безопасным.
supercat
0

Может, программисты думали примерно так ...

!! myAnswer является логическим. В контексте, это должно стать логическим, но я просто люблю грохнуть, чтобы убедиться, потому что однажды была таинственная ошибка, которая меня укусила, и ба-бах, я ее убил.

dongilmore
источник
0

Это может быть примером трюка двойного взрыва , подробнее см . Идиома Safe Bool . Резюмирую первую страницу статьи.

В C ++ существует несколько способов предоставления логических тестов для классов.

Очевидный способ - operator boolоператор преобразования.

// operator bool version
  class Testable {
    bool ok_;
  public:
    explicit Testable(bool b=true):ok_(b) {}

    operator bool() const { // use bool conversion operator
      return ok_;
    }
  };

Мы можем протестировать класс,

Testable test;
  if (test) 
    std::cout << "Yes, test is working!\n";
  else 
    std::cout << "No, test is not working!\n";

Однако opereator boolсчитается небезопасным, поскольку допускает бессмысленные операции, такие как test << 1;или int i=test.

Использование operator!более безопасно, потому что мы избегаем проблем с неявным преобразованием или перегрузкой.

Реализация тривиальна,

bool operator!() const { // use operator!
    return !ok_;
  }

Два идиоматических способа проверки Testableобъекта:

  Testable test;
  if (!!test) 
    std::cout << "Yes, test is working!\n";
  if (!test2) {
    std::cout << "No, test2 is not working!\n";

Первая версия if (!!test)- это то, что некоторые называют трюком двойного взрыва .

kgf3JfUtW
источник
1
Начиная с C ++ 11, можно использовать explicit operator boolдля предотвращения неявного преобразования в другие целочисленные типы.
Арне Фогель