Не знаю что значит "строгость была признана избыточной". Даже в современном языке Rust неинициализированную память читать нельзя.
В С и С++ вам неминуемо приходится сталкиваться с неинициализированной памятью. Например, байты-заполнители (padding bytes), добавленные компилятором в struct-тип между его полями для обеспечения правильного выравнивания полей в общем случае являются неинициализированной памятью. И вы можете видеть и читать эти байты путем рассмотрения struct-объекта как массива байтов
unsigned char[] или при копировании таких объектов через
memcpy. Память, выделенная
malloc тоже является неинициализированной и ограничивать работу с этой памятью правилами "читать можно только те байты, который ранее были записаны" - это тоже чересчур ограничивающее правило для эффективной низкоуровневой работы с памятью.
В С и С++ разделяют понятия
1) самого факта чтения неинициализированной переменной, и
2) использования прочитанного значения в дальнейшем коде
* В "классическом" С (первый стандарт С89/90) все бело четко: уже сам факт чтения неинициализированной переменной "мгновенно" приводил к неопределенному поведению. И до свидания. То есть сама попытка чтения любой неинициализированной переменной могла привести к пресловутому неожиданному "форматированию жесткого диска".
* В С99 это сочли слишком строгим и перекроили спецификацию так: чтение неинициализированной переменной дает либо
неспецифицированное значение, либо т.наз.
trap representation. В последнем случае, то есть при попытке чтения trap representation, сразу возникает неопределенное поведение. В первом случае неопределенного поведения не возникает, т.е. вы просто тихо получаете на руки какое-то неспецифицированное значение.
Ключевой момент тут заключается в том, что некоторые типы могут вообще не иметь trap representations. Например,
unsigned char гарантированно не имеет trap representations. То есть в рамках таких правил неинициализированную переменную типа
unsigned char можно спокойно читать - это не приводит к неопределенному поведению. Разумеется, само полученное значение не специфицировано, т.е. непредсказуемо. В результате при дальнейшей работе с этим значением вы получите неспецифицированное (но не неопределенное!) поведение. То есть предсказать, как будут работать ваши if-ы на неинициализированном
unsigned char нельзя, но и внезапного "форматирования жесткого диска" произойти уже не может.
Например, кот такая функция в C99 не порождает неопределеного поведения и заведомо возвращает 0
Код:
// C99
int foo(void)
{
unsigned char a; // Неинициализированная переменная, не имеющая trap representations
a *= 2; // Неспецифицированное, но заведомо четное значение
return a % 2; // Заведомо 0
}
Также было специально оговорено, что объекты struct-типов, рассматриваемые целиком, никогда не имеют trap representations. Что касается фундаментальных типов, то какие типы в вашей реализации имеют trap representations, а какие нет - зависит от реализации.
* В С11 решили, что и эта спецификация тоже не совсем адекватна: она не очень хорошо согласуется с теговыми регистрами на архитектурах вроде Itanium. На этой архитектуре всегда имело смысл помечать регистры процессора, содержащие неинициализированные значения, тегом NaT ("Not A Thing"), что приводило к исключению при попытке чтения такого неинициализированного значения. Это было весьма полезной фичей. А теперь С99 заявил, что для некоторых типов неинициализированные значения все таки "можно" читать, что в широком ряде случаев заставляет реализации насильно сбрасывать тег NaT для неинициализированных регистров, тратя на это процессорные циклы и принижая защитную ценность этой фичи.
Тогда вспомнили, что в С99 разрешение на чтение неинициализированных переменных вводилось в язык именно ради того, чтобы разрешить доступ к неинициализированный
памяти, а на неинициализированные регистры процессора распространять эти разрешения смысла нет. И спецификацию подправили следующим образом: если ваша переменная является автоматической и используется в коде так, что ее теоретически можно объявить с классом памяти
register (т.е. к ней некогда не применяется оператор взятия адреса
&), то на нее распространяются "классические" строгие требования С89/90, т.е. любое чтение такой неинициализированной переменной - сразу неопределенное поведение. В противном случае для нее работают "расслабленные" требования С99 (
http://port70.net/~nsz/c/c11/n1570.html#6.3.2.1p2). Это последнее исправление известно под неформальным именем "Itanium clause".
Например:
Код:
// C11
void foo(void)
{
unsigned char a, b; // Неинициализированные переменные, не имеющие trap representations
&b;
a += a; // <- Неопределенное поведение
b += b; // <- Здесь нет неопределенного поведения
}
В таком виде нынешняя спецификация выглядит странновато, но, как видите, на то есть причины.
Вышеописанное относится к С. В С++ используют другой подход к спецификации, за которым я пристально не следил. Надо бы освежить свои познания на тему того, чего там нагородили.