Здравствуйте, rg45, Вы писали:
N>>Это я подпилил на сравнение по методу rg45 вместо прежней конверсии к int64 и выходу за границы:
R>Гм, а зачем приводить слагаемые к __int32?
Так в оригинале. Это код конкретно для случая template < typename T, typename U > class AdditionHelper < T, U, AdditionState_CastInt64CheckMinMax >, который назначен вызываться для сложения двух SafeInt<int>. По-нормальному надо было бы породить отдельный шаблонный класс, но мне было облом.
R> А что будет в случае, если T — беззнаковый 32-битный тип?
В данном конкретном SafeInt3.hpp вызовётся метод другого шаблонного класса, который я не трогал.
R> В результате интерпретации беззнакового типа как знакового возможны две ошибочные ситуации:
Здравствуйте, rg45, Вы писали:
R>Как-то так:
[...] R>Будет работать и для знаковых и для беззнаковых.
Всё однако хитрее. Может, я поспешил с похвалой?
Clang 3.0 на 32-битке при -O (и выше) твоё выражение под assert'ом считает истинным *всегда*, соответственно проверка не происходит.
Это независимо от того, используется ли '>' или '>=' в обоих сравнениях.
В коде, если компилировать вместо assert передачу этого выражения как возврата, видно, что загружается просто константа 1.
На 64-битке такой проблемы нет, проверки честно производятся.
Проверочная программа, чуть более обширная.
Первый файл (разделено, чтобы не инлайнило):
extern void p(int a, int b);
int main() {
p(1, 1);
p(0x30000000, 0x30000000);
p(0x40000000, 0x40000000);
p(0x50000000, 0x50000000);
p(0xB0000000, 0xB0000000);
p(0xC0000000, 0xC0000000);
p(0xD0000000, 0xD0000000);
p(-1, -1);
return 0;
}
Второй файл:
#include <stdio.h>
#include <limits.h>
void p(int a, int b)
{
int result = a + b;
int x1 = a ^ b;
int ovf1, ovf2, ovf3, ovf4;
if (x1 >= 0)
ovf1 = ((a ^ result) < 0);
else
ovf1 = 0;
if (b >= 0)
ovf2 = (a > INT_MAX - b);
else
ovf2 = (a < INT_MIN - b);
ovf3 = ((b >= 0) != (result >= a));
ovf4 = ((b > 0) != (result > a));
printf("%8x %8x %8x %1d:%1d:%1d:%1d\n", a, b, result,
ovf1, ovf2, ovf3, ovf4);
}
Результат под gcc везде, под open64 (он только на 64 бывает), под clang на 32 битах без оптимизации, и под clang на 64 битах:
Здравствуйте, netch80, Вы писали:
N>Всё однако хитрее. Может, я поспешил с похвалой?
Каждая похвала на учете?
N>Clang 3.0 на 32-битке при -O (и выше) твоё выражение под assert'ом считает истинным *всегда*, соответственно проверка не происходит. N>Это независимо от того, используется ли '>' или '>=' в обоих сравнениях.
Забавно. Но имеет право, поскольку, в отсутствие переполнения это выражение действительно всегда истинно, а при переполнении компилятор с оптимизатором делают что хотят, потому как UB
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
N>>Clang 3.0 на 32-битке при -O (и выше) твоё выражение под assert'ом считает истинным *всегда*, соответственно проверка не происходит. N>>Это независимо от того, используется ли '>' или '>=' в обоих сравнениях.
R>Забавно. Но имеет право, поскольку, в отсутствие переполнения это выражение действительно всегда истинно, а при переполнении компилятор с оптимизатором делают что хотят, потому как UB
А что если попробовать обмануть оптимизатор?
// Определение этой константы вынесено
// в отдельную единицу трансляции, чтобы
// оптимизатор не видел ее значения.extern const unsigned char zero;
template<typename T>
T add (T a, T b)
{
T c = a + b;
assert(b >= zero == c >= a);
return c;
}
Конечно, это уже не так заманчиво, как выглядело раньше.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>>Забавно. Но имеет право, поскольку, в отсутствие переполнения это выражение действительно всегда истинно, а при переполнении компилятор с оптимизатором делают что хотят, потому как UB
R>А что если попробовать обмануть оптимизатор?
Не помогает Но очень своеобразно. Вот сишный код и разборка ассемблера.
extern const int foobar;
int ovf3(int a, int b)
{
int result = a + b;
return ((b >= foobar) != (result >= a));
}
Получаем:
000000a0 <ovf3>:
a0: 8b 44 24 08 mov 0x8(%esp),%eax ; это прочли b
a4: 89 c1 mov %eax,%ecx ; скопировали в %ecx
a6: c1 e9 1f shr $0x1f,%ecx ; оставили от него один знак (то есть b<0)
a9: 39 05 00 00 00 00 cmp %eax,0x0 ; внешняя переменная foobar, адрес пока неизвестен
af: 0f 9e c0 setle %al ; al - результат (b<foobar)
b2: 0f b6 c0 movzbl %al,%eax ; растянули его до полного %eax
b5: 31 c8 xor %ecx,%eax ; неравенство двух тестов, то есть (b<0) != (b<foobar)
b7: 83 f0 01 xor $0x1,%eax ; булевская инверсия - превратили в равенство тестов
ba: c3 ret
Получается, что clang из (result >= a) сделал ((a+b)>=a), это очевидно упростил до (b>=0), ну а дальше очевидно, что проверка всегда даст (b>=0) == (b>=foobar), если foobar == 0
Так что "в живых" остались сравнение по XOR и по двум границам сравнения с пределами.
Здравствуйте, rg45, Вы писали:
N>>Всё однако хитрее. Может, я поспешил с похвалой? R>Каждая похвала на учете?
Нет, но если похвалил решение (не человека), а дальше нашлись проблемы с этим решением — то что ещё сказать?
R>Забавно. Но имеет право, поскольку, в отсутствие переполнения это выражение действительно всегда истинно, а при переполнении компилятор с оптимизатором делают что хотят, потому как UB
Вот честно жаль, что нету поддержки в библиотеке.
Какие-то несчастные div() есть, а поддержки простейшего сложения с детектом переполнения — нет.
Здравствуйте, netch80, Вы писали:
N>... N>Получается, что clang из (result >= a) сделал ((a+b)>=a), это очевидно упростил до (b>=0), ну а дальше очевидно, что проверка всегда даст (b>=0) == (b>=foobar), если foobar == 0
Мда, жестокий оптимизатор, ничего не скажешь. Выходит, что в самом общем случае, использовать результат сложения вообще нельзя, поскольку в случае переполнения его значение может оказаться любым, и кто знает, каким образом этот факт будет учтен тем или другим компилятором. В общем, UB — оно и в Африке UB. Кстати, вариант с xor тоже использует результат сложения, поэтому он также чреват.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
N>>... N>>Получается, что clang из (result >= a) сделал ((a+b)>=a), это очевидно упростил до (b>=0), ну а дальше очевидно, что проверка всегда даст (b>=0) == (b>=foobar), если foobar == 0 R>Мда, жестокий оптимизатор, ничего не скажешь. Выходит, что в самом общем случае, использовать результат сложения вообще нельзя, поскольку в случае переполнения его значение может оказаться любым, и кто знает, каким образом этот факт будет учтен тем или другим компилятором.
Так-то оно так, но на практике я пока не видел реализаций, где не выполнялось бы правило, что сумма двух чисел размером N (после штатного расширения, например, short -> int), *приведённая к тому же типу* (этот момент тут принципиален), не давала бы N младших бит суммы. Но на практике сейчас невероятно встретить реализацию иную, чем "дополнение до 2", поэтому неудивительно, что оно так.
R> В общем, UB — оно и в Африке UB. Кстати, вариант с xor тоже использует результат сложения, поэтому он также чреват.
Да, но вероятность этой чреватости даже не в разы, а на порядки ниже.
Пока что я не вижу потенциального UB только у двух не-ассемблерных реализаций — с контр-вычитанием (ovf2 в прошлом примере) и с расширением разрядности (как в SafeInt<>), но последняя значительно дороже для всего шириной в int и выше.
Кстати, мы пока что обсуждали только сложение, но есть ещё тема умножения. И вот с ним всё мрачнее — я пока не представляю себе иных вариантов, кроме как ассемблер или проверка на расширенной разрядности.