Беззнаковый тип (unsigned type) хранит только неотрицательные числа. Бит, который у знакового типа уходил на знак, здесь работает на величину: диапазон unsigned int — от 0 до 4 294 967 295 вместо −2.1·10⁹…2.1·10⁹.

unsigned int count = 0;
unsigned long long big = 0ULL;

Переполнение здесь определено

Это главное отличие от знаковых целых типов. Переполнение знакового целого — Неопределённое поведение. Переполнение беззнакового — нормальная, описанная в стандарте операция: вычисления в типе ширины N идут по модулю 2^N, значение «заворачивается». N здесь — ширина в битах, то есть 32 для обычного unsigned int.

unsigned int x = 0;
std::cout << x - 1 << '\n';   // 4294967295, а не -1

Для 32-битного unsigned int 3 000 000 000 + 3 000 000 000 ≡ 1 705 032 704 (mod 2³²), и этот результат одинаков на любой такой системе.

Но «поведение определено» не означает «алгоритм правильный». Если мы считали количество объектов и после прибавления единицы получили ноль, это всё ещё ошибка задачи — просто воспроизводимая.

Отсюда правило: вычитание из беззнакового нуля не даёт отрицательного числа. Оно даёт огромное положительное. Цикл, который отсчитывает назад до нуля, на беззнаковом счётчике не завершается никогда:

for (unsigned int i = 10; i >= 0; --i) {  // условие истинно всегда
  // ...
}

Смешивание со знаковыми

Если в одном выражении встречаются знаковый и беззнаковый типы одинакового ранга, знаковый молча превращается в беззнаковый — и сравнение переворачивается:

int a = -1;
unsigned int b = 1;
if (a < b) {        // ложь: -1 превращается в 4294967295
  // сюда не попадём
}

Компилятор об этом предупреждает, если попросить: -Wall -Wextra выдаёт comparison of integer expressions of different signedness. Предупреждения компилятора здесь — не придирки, а ровно тот случай, ради которого они есть.

Правило «при смешивании всегда побеждает беззнаковый» при этом неверно: если знаковый тип имеет больший ранг и вмещает весь диапазон беззнакового, выбирается знаковый. Полная схема — Арифметические преобразования типов.

Ещё одна оговорка: правило про арифметику по модулю относится к типу, в котором выполняется операция. Малые беззнаковые типы сначала проходят продвижение, и unsigned short на обычной системе продвигается до знакового int — арифметики по модулю там уже не будет.

Когда использовать

Соблазнительно объявлять unsigned всё, что «не бывает отрицательным»: возраст, размер, количество. Google C++ Style Guide советует так не делать — беззнаковость не защищает от отрицательных значений, она превращает их в гигантские положительные, то есть меняет явную ошибку на скрытую.

Беззнаковые уместны, когда:

  • нужна битовая арифметика: маски, сдвиги, побитовые операции;
  • нужно определённое поведение при переполнении (хеши, циклические счётчики);
  • работаете с типами стандартной библиотеки, которые сами беззнаковые (size_t, .size() у контейнеров).

Частые ошибки

  • Обратный цикл for (unsigned i = n; i >= 0; --i) — бесконечный.
  • Сравнение i < v.size() при знаковом i — предупреждение о signedness, а при отрицательном i ещё и неверный результат.
  • Ожидание, что unsigned запретит присвоить отрицательное значение: unsigned int x = -1; компилируется и даёт максимум типа.
  • Считать, что беззнаковый тип делает безопасным деление на ноль. Не делает: это Неопределённое поведение независимо от знаковости.