Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Cast uninitialized variable to (void)
Is it undefined behaviour to cast an uninitialized variable to (void)?
Example:
int main()
{
int x;
(void)x;
return 0;
}
4 answers
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
Estela | (no comment) | Sep 12, 2022 at 16:32 |
It depends. This boils down to whether or not the expression cast to void
contains any side effects, such as accessing a volatile
-qualified object or modifying any object.
C17 6.3.2.2:
If an expression of any other type is evaluated as a void expression, its value or designator is discarded. (A
void
expression is evaluated for its side effects.)
In your example the cast operand does not contain any side effects so it is simply discarded, meaning that it is safe.
This would not be fine, however:
{
volatile int x;
(void)x; // undefined behavior, lvalue access of local uninitialized variable
}
While this is ok:
{
int x;
(void)(x=1); // well-defined, x is set to 1
(void)function(x); // well-defined, function is called and the result discarded
}
Regarding all the details of when it is undefined behavior to use an uninitialized variable with an indeterminate value, check out this answer: (Why) is using an uninitialized variable undefined behavior? The explanation is not as trivial as "this is always ok" or "this is always UB". The type of the variable matters a lot - plain integers are for example very unlikely to contain trap representations, but floating-point variables or pointers (particularly) may do.
So this is not ok either:
{
int x,y;
(void)(x=y); // undefined behavior, lvalue access of local uninitialized variable y
}
1 comment thread
Yes, it is safe unless the variable is volatile.
The term used in the C99 standard for the value of an "uninitialized" variable is indeterminate.
From C99 standard:
Section 6.2.4 Storage durations of objects (emphasis mine):
5 For such an object that does not have a variable length array type, its lifetime extends from entry into the block with which it is associated until execution of that block ends in any way. (Entering an enclosed block or calling a function suspends, but does not end, execution of the current block.) If the block is entered recursively, a new instance of the object is created each time. The initial value of the object is indeterminate. If an initialization is specified for the object, it is performed each time the declaration is reached in the execution of the block; otherwise, the value becomes indeterminate each time the declaration is reached.
Section 3.17.2 indeterminate value
either an unspecified value or a trap representation
Section 3.17.3 unspecified value
valid value of the relevant type where this International Standard imposes no requirements on which value is chosen in any instance
If the initial value of the object is unspecified then the code is OK. No undefined behaviour. The cast to (void) is irrelevant in this case, we can even read an uninitialized variable with an unspecified value safely.
But, what if the variable is initialized to a trap representation?
Section 6.2.6 Representations of types - 6.2.6.1 General
5 Certain object representations need not represent a value of the object type. If the stored value of an object has such a representation and is read by an lvalue expression that does not have character type, the behavior is undefined. If such a representation is produced by a side effect that modifies all or any part of the object by an lvalue expression that does not have character type, the behavior is undefined. (note 41) Such a representation is called a trap representation.
note 41 : Thus, an automatic variable can be initialized to a trap representation without causing undefined behavior, but the value of the variable cannot be used until a proper value is stored in it.
When casting the value to (void) we are not reading it by an lvalue expression. Hence it is safe.
Unless it is a volatile variable. In that case, it is lvalue access and it would be undefined behaviour. The excellent answer by Lundin has details on that.
2 comment threads
It is undefined according to my reading of the standard (I am referring to the latest public draft).
int x;
(void)x;
In the second line, (void)x
is a full expression, but x
by itself is already an expression. For the expression x
the following holds:
6.5.1 - 2: An identifier is a primary expression, provided it has been declared as designating an object (in which case it is an lvalue) or a function (in which case it is a function designator).
Thus, x
being an identifer that is declared to designate an int
object implies that x
is a primary expression and an lvalue.
6.3.2.1 - 2: Except when it is the operand of the sizeof operator, the unary & operator, the ++ operator, the -- operator, or the left operand of the . operator or an assignment operator, an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue); this is called lvalue conversion. [...]
As the lvalue x
is the operand of a cast operator (which is not in the above exception list) and as it is also no array type it is converted into the value stored in the object designated by x
.
6.3.2.1 - 2: [...] If the lvalue designates an object of automatic storage duration that could have been declared with the register storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined.
As x
has automatic storage duration, has never its address taken and is uninitialized, the behavior is undefined. (The point about the register
storage class is to allow optimizations on certain processors. See the description of the "NaT" register state in this article about the Itanium processor.)
Note that it does not matter that x
is part of a larger expression (void)x
. But, just to extend on that aspect:
6.5 - 1: An expression is a sequence of operators and operands that specifies computation of a value, or that designates an object or a function, or that generates side effects, or that performs a combination thereof. The value computations of the operands of an operator are sequenced before the value computation of the result of the operator.
Important is the sequencing aspect here: The value of the operand x
is computed before the result of the (void)
operator is computed. The full expression will - in the end - have type void
.
6.3.2.2 - 1: The (nonexistent) value of a void expression (an expression that has type void) shall not be used in any way, and implicit or explicit conversions (except to void) shall not be applied to such an expression. [...]
But this only says that you are not allowed to make further use of that void
"result" of the expression (void)x
.
6.3.2.2 - 1: [...] If an expression of any other type is evaluated as a void expression, its value or designator is discarded. (A void expression is evaluated for its side effects.)
Note that this does not say that the whole expression is discarded: Only its value (which is the result of the expression being evaluated) is discarded. The clause is there to make it clear that a void expressions is evaluated. The sentence in parentheses gives the reason why it is evaluated, namely because it may have side effects. The sentence does not say that it is only evaluated if it has side effects.
Therefore, it is also not possible to argue that the "undefinedness" stated above becomes irrelevant because the whole expression would be discarded: The expression just won't be discarded, only its result.
0 comment threads
Well, an "uninitialized" variable is, in fact, not uninitialized as that terminology may suggest; it is just filled with random garbage that happened to be on that memory section. That is to say, uninitialized variables are not special, they just happen to not contain any meaningful value, but they do have a value.
void
is not a type, rather the contrary -- it's the absence of type. Knowing this, casting to it wouldn't make much sense but as it happens, it is allowed; it simply discards the expression being cast.¹²
Now, casting to a void pointer (void*
) is another thing. It's mostly used as a sort of "generics" in C (e.g, the standard qsort
function uses this). Since these pointers are aligned as character ones³ and can be cast to any other pointer type, you can write functions that take void pointers but cast them to their concrete types inside, thus allowing a generic-like function signature (naturally, a whole lot less safe than what you may find in other languages).
Null pointers are essentially void pointers to an integer constant expression of value 0 and have the interesting property of being guaranteed that when cast to concrete pointer types, they will never be considered equal.⁴
Hope this answers your question, if not let me know in the comments :)
¹ From C's standard, section 6.3.2.2:
If an expression of any other type is evaluated as a void expression, its value or designator is discarded. (A void expression is evaluated for its side effects.)
² From C++'s standard, section 5.2.9.4:
Any expression can be explicitly converted to type cv void." The expression value is discarded.
³ From C's standard, section 6.3.2.3:
A pointer to void may be converted to or from a pointer to any incomplete or object type. A pointer to any incomplete or object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
And 6.2.5.27:
A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.
⁴ From C's standard, section 6.3.2.3:
An integer constant expression with the value 0, or such an expression cast to type
void *
, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
2 comment threads