Reputation: 61
I would like to ask why using variables that are not initialized is considered non type-safe?
I'm reading Bjarne Stroustrup's beginner book(Programming Principles and Practice Using C++) from the C++ book guide on this site.
There is a part in the book about type-safety that states :
A program - or a part of a program - is type-safe when objects are used only according to the rules for their type. For example, using a variable before it has been initialized is not considered type-safe.
Then the book provides the following code as an example:
int main() {
double x; // we "forgot" to initialize
// the value of x is undefined
double y = x; // the value of y is undefined
double z = 2.0+x; // the meaning of + and the value of z are undefined
}
I understand that a local variable that is not initialized will have an indeterminate value and reading this variable will cause undefined behavior. What I do not understand is how is it connected to type-safety. We still know the types from the variable's definition.
Why does the comment in the above code states that the meaning of + is undefined when both 2.0 and x are double, and the + is defined for double + double?
Upvotes: 5
Views: 1098
Reputation: 151
@codekaizer and @Shankar are right: undefined behavior is, by definition, not type safe behavior. How that applies to primitive types is a little harder to wrap your head around, though. It seems reasonable that any appropriately long sequence of bits could be a valid int
. As @BoPersson pointed out below, this is not strictly true and implementations are free to include values which cause interrupts under arithmetic. For integers this practically only applies to 0 when used to divide, but that does not mean the standard doesn't allow for an integer version of something like floating point NaN
on a suitably unusual architecture.
The reader may find an example with virtual functions more intuitively illustrative of why uninitialized variables aren't type safe. Consider:
struct Base {
virtual int foo() const =0;
};
struct DerivedA : public Base {
int foo() const override { return 10; }
};
struct DerivedB : public Base {
int foo() const override { return -10; }
};
int main() {
Base* abstractStructPtr;
std::cout << abstractStructPtr->foo() << std::endl;
return 0;
}
The type of abstractStructPtr
means you can call foo()
on it. The expression is valid: abstractStructPtr
has a type, that is why you can call foo()
. However, the implementation of foo()
lives in derived classes.
Since abstractStructPtr
isn't initialized, the data it points to isn't guaranteed to be structured in such a way that it can fulfill the call to foo()
. In other words, while the type of absractStructPtr
is Base*
, there is no guarantee that the data that is pointed to is actually a Base
object of any kind. Calling foo()
is thus undefined behavior and not type safe. Anything could happen; practically it will probably just crash via a memory access violation, but it might not! Kablooey.
Upvotes: 2
Reputation: 12174
Undefined behavior means the output could be what you expect or some indeterminate value that may be outside the valid range of a type.
One clear example of undefined behavior is signed integer overflow:
unsigned int i; // uninitialized
int x = i + 2; // indeterminate value
if (x + 1 > x) {} // undefined behavior due to signed overflow
x
can have a value outside int
valid range if i
holds a max value of unsigned int
.
Thus, type safety is not guaranteed for expressions having indeterminate values.
Upvotes: 2
Reputation:
Even though 'x' was declared double, since it was not initialized, it has a random bit pattern in memory and that pattern might not represent any valid double precision number. Hence the "meaning of z" is undefined.
Upvotes: 0