size_t
отсутствие подписи - это в основном историческая случайность - если ваш мир 16-битный, переход от 32767 к 65535 максимальному размеру объекта является большой победой; в современных вычислительных системах (где 64 и 32-битные являются нормой) тот факт, что size_t
без знака, в основном неприятен.
Хотя неподписанные типы имеют меньше неопределенное поведение (поскольку гарантируется перенос), тот факт, что они имеют в основном семантику "битового поля", часто является причиной ошибок и других неприятных сюрпризов; в частности:
Разница между значениями без знака также без знака, с обычной семантикой с циклическим переходом, поэтому, если вы можете ожидать отрицательного значения, вы должны привести его заранее;
unsigned a = 10, b = 20;
// prints UINT_MAX-10, i.e. 4294967286 if unsigned is 32 bit
std::cout << a-b << "\n";
в общем, в знаковых / беззнаковых сравнениях и математических операциях выигрыши без знака (поэтому значение со знаком преобразуется в беззнаковое неявное значение), что, опять же, приводит к неожиданностям;
unsigned a = 10;
int b = -2;
if(a < b) std::cout<<"a < b\n"; // prints "a < b"
в обычных ситуациях (например, итерация в обратном направлении) семантика без знака часто проблематична, так как вы хотите, чтобы индекс стал отрицательным для граничного условия
// This works fine if T is signed, loops forever if T is unsigned
for(T idx = c.size() - 1; idx >= 0; idx--) {
// ...
}
Кроме того, тот факт, что значение без знака не может принимать отрицательное значение, в основном является соломенным; вы можете избежать проверки на наличие отрицательных значений, но из-за неявных преобразований со знаком и без знака это не остановит никаких ошибок - вы просто перекладываете вину на себя. Если пользователь передает отрицательное значение в вашу библиотечную функцию, принимая size_t
, оно просто станет очень большим числом, которое будет таким же неправильным, если не хуже.
int sum_arr(int *arr, unsigned len) {
int ret = 0;
for(unsigned i = 0; i < len; ++i) {
ret += arr[i];
}
return ret;
}
// compiles successfully and overflows the array; it len was signed,
// it would just return 0
sum_arr(some_array, -10);
Для части оптимизации: преимущества подписанных типов в этом отношении переоценены; да, компилятор может предположить, что переполнение никогда не произойдет, поэтому он может быть очень умным в некоторых ситуациях, но, как правило, это не изменит игру (так как в общем случае семантика циклического перехода «бесплатна» в современных архитектурах); самое главное, как обычно, если ваш профилировщик обнаружит, что определенная зона является узким местом, вы можете изменить ее, чтобы она работала быстрее (включая переключение типов локально, чтобы компилятор генерировал лучший код, если вы считаете это выгодным).
Короче говоря: я бы пошел на подпись, но не по соображениям производительности, а потому, что семантика, как правило, менее удивительна / враждебна в большинстве распространенных сценариев.