Я недавно вырвал свои волосы, отлаживая этот кусок кода (слегка модифицированный для простоты изложения):
char *packedData;
unsigned char* indexBegin, *indexEnd;
int block, row;
// +------ bad!
// v
int cRow = std::upper_bound( indexBegin, indexEnd, row&255 ) - indexBegin - 1;
char value = *(packedData + (block + cRow) * bytesPerRow);
Конечно, присвоение разности двух указателей (результат std::upper_bound
минус начало искомого массива) для int, а не ptrdiff_t, является неправильным в 64-битной среде, но это особенно плохое поведение это было очень неожиданно. Я ожидал, что это не получится, если размер массива в [indexBegin, indexEnd) будет больше 2 ГБ, так что разница превысит int; но на самом деле произошел сбой, когда indexBegin и indexEnd имели значения на противоположных сторонах 2 ^ 31 (т.е. indexBegin = 0x7fffffe0, indexEnd = 0x80000010). Дальнейшие исследования выявили следующий код сборки x86-64 (сгенерированный MSVC ++ 2005 с оптимизацией):
; (inlined code of std::upper_bound, which leaves indexBegin in rbx,
; the result of upper_bound in r9, block at *(r12+0x28), and data at
; *(r12+0x40), immediately precedes this point)
movsxd rcx, r9d ; movsxd?!
movsxd rax, ebx ; movsxd?!
sub rcx, rax
lea rdx, [rcx+rdi-1]
movsxd rax, dword ptr [r12+28h]
imul rdx, rax
mov rax, qword ptr [r12+40h]
mov rcx, byte ptr[rdx+rax]
Этот код обрабатывает из вычитаемых указателей как 32-разрядные значения со знаком, расширяя их в знак в 64-разрядные регистры перед их вычитанием и умножая результат на другое 32-разрядное значение со знаком, расширенным знаком, и затем индексирует другой массив с 64-битным результатом этого вычисления. Как ни старайся, я не могу понять, под какой теорией это когда-либо будет правильным. Если бы указатели были вычтены как 64-битные значения, или была другая инструкция, сразу после imul, это edx с расширенным знаком в rdx (или последний финальный mov ссылался на rax + edx, но я не думаю, что это доступно в x86-64), все будет хорошо (номинально опасно, но я знаю, что [indexBegin, indexEnd) никогда не достигнет 2 ГБ в длину).
Вопрос несколько академический, поскольку моя настоящая ошибка легко исправляется с помощью простого использования 64-битного типа для хранения различий указателей, но это ошибка компилятора или какая-то неясная часть спецификации языка, которая позволяет компилятор предположить, что операнды вычитания будут индивидуально вписываться в тип результата?
РЕДАКТИРОВАТЬ : единственная ситуация, о которой я могу подумать, это сделать то, что компилятор сделал хорошо, если допустить, что целочисленные переполнения никогда не произойдут (так что если я вычту два числа и назначу в результате signed int
компилятор может свободно использовать больший целочисленный тип со знаком, что в данном случае оказывается неверным). Это разрешено спецификацией языка?