Undefined behavior inside void expressions
Asked Answered
C

1

7

Is a C implementation required to ignore undefined behaviors occurring during the evaluation of void expressions as if the evaluation itself never took place?

Considering C11, 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.)

This is related to the common idiom used to prevent compilers from warning about unused variables:

void f() {
  int a;
  (void)a;
}

But what if we have undefined behavior, such as:

void f() {
  int a;
  (void)(1/0);
}

Can I safely claim that this program contains no undefined behavior? The standard says that "its value or designator is discarded", but the "expression (...) is evaluated (...)", so the evaluation does seem to take place.

GCC/Clang do report the undefined behavior, since it is obvious in this case, but in a more subtle example they don't:

int main() {
  int a = 1;
  int b = 0;
  (void)(a/b);
  return 0;
}

Even with -O0, neither GCC nor Clang evaluate 1/0. But that also happens even without the cast to void, so it's not representative.

Pushing the argument to its extreme, wouldn't the simple evaluation of (void)a in my first example (where a is uninitialized) systematically trigger undefined behavior?

ISO C11 6.3.2.1 §2 does mention that:

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.

However, in the Annex J.2 Undefined behavior, the phrasing is slightly different:

The behavior is undefined in the following circumstances:

(...)

An lvalue designating an object of automatic storage duration that could have been declared with the register storage class is used in a context that requires the value of the designated object, but the object is uninitialized. (6.3.2.1).

This annex does lead to the interpretation that a void expression containing undefined behavior during its evaluation is not actually evaluated, but since it's just an annex, I'm not sure of its argumentative weight.

Carafe answered 24/7, 2019 at 12:12 Comment(11)
Hmm, even the (void)a is in grey area. 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.)"Landslide
The abstract machine would evaluate the expression before discarding the result.Musk
Have a look at this: https://www.godbolt.org/z/4tHKGg : actually the expression is evaluated partially, that is the f function is called three times, but the division and the addition in the expression are not evaluated (because the result is ignored). But IMO a compiler could generate the code for the division and the addition and then ignore the result.Revocation
@IanAbbott A conforming implementation can perform the evaluation, but 5.1.2.3§4 leaves some freedom: "An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced".Bourbonism
The answer to the question in the first sentence, “Can undefined behaviors occurring during the evaluation of void expressions be ignored as if the evaluation itself never took place?”, is undoubtedly yes. The C standard imposes no requirements on undefined behavior, so ignoring the undefined behavior is a conforming action by a C implementation. I do not think that is the intent of this Stack Overflow question, though—I think anol seeks to ask whether the user of the implementation may rely on the undefined behavior having no effect; is the C implementation required to ignore it.…Zeculon
@Virgule An actual implementation could evaluate the unneeded parts of the expression as per the abstract machine, so if it's undefined behavior for the actual machine, it's undefined behavior full stop.Musk
… If so, then you might wish to edit the first sentence.Zeculon
@EricPostpischil : indeed, thanks for the remark, I changed it. If the formulation is still not right, please fix it to correspond to what you understood in your comment (which is what I meant).Carafe
UB can have side effects that are undefined :-)Tessietessier
@IanAbbott yes, as long as you're talking about the abstract machine (as opposed to an implementation thereof), you're right, evaluation will occur. I somehow misread your original comment, sorry.Bourbonism
@Virgule Actually, I had a typo/braino in my previous comment; When I wrote "so if it's undefined for the actual machine, it's undefined behavior full stop", I meant the abstract machine not the actual machine.Musk
L
5

This is related to the common idiom used to prevent compilers from warning about unused variables:

void f() {
  int a;
  (void)a;
}

Yes and no. I'd argue that that idiom turns an unused variable into a used one -- it appears in an expression -- with the cast to void serving to prevent compilers from complaining about the result of that expression going unused. But in the technical, language-lawyer sense, that particular expression of the idiom produces UB because the sub-expression a is subject to lvalue conversion when a's value is indeterminate. You've already quoted the relevant text of the standard.

But what if we have undefined behavior, such as:

void f() {
  int a;
  (void)(1/0);
}

Can I safely claim that this program contains no undefined behavior?

No.

The standard says that "its value or designator is discarded", but the "expression (...) is evaluated (...)", so the evaluation does seem to take place.

Yes, just as the expression a in your earlier example is also evaluated, also producing UB. UB arises from evaluation of the inner sub-expression. The conversion to type void is a separable consideration, exactly as a conversion to any other type would be.

GCC/Clang do report the undefined behavior, since it is obvious in this case, but in a more subtle example they don't:

Compiler behavior cannot be taken as indicative here. C does not require compilers to diagnose most undefined behaviors, not even those that could, in principle, be detected at compile time. Indeed, it is important to recognize that UB arising from incorrect code happens first and foremost at compile time, though of course it follows that if an executable is produced then it exhibits UB, too.

Pushing the argument to its extreme, wouldn't the simple evaluation of (void)a in my first example (where a is uninitialized) systematically trigger undefined behavior?

Yes, as I already remarked. But that does not mean that programs containing such constructions are obligated to misbehave. As a quality-of-implementation matter, I think it reasonable to hope that the expression statement (void)a; will be accepted by the compiler and will have no corresponding runtime behavior at all. But I cannot rely on the language standard to back me up on that.

This annex does lead to the interpretation that a void expression containing undefined behavior during its evaluation is not actually evaluated, but since it's just an annex, I'm not sure of its argumentative weight.

The plain wording of the normative text of the standard is quite sufficient here. The annex is not normative, but if there is any question about how the normative text is meant to be interpreted, then informative sections of the standard, such as Annex J, are one of the sources taken into account in sorting that out (but they are still only informative).

Labrie answered 24/7, 2019 at 12:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.