Related
Consider this simple code:
void g();
void foo()
{
volatile bool x = false;
if (x)
g();
}
https://godbolt.org/z/I2kBY7
You can see that neither gcc nor clang optimize out the potential call to g. This is correct in my understanding: The abstract machine is to assume that volatile variables may change at any moment (due to being e.g. hardware-mapped), so constant-folding the false initialization into the if check would be wrong.
But MSVC eliminates the call to g entirely (keeping the reads and writes to the volatile though!). Is this standard-compliant behavior?
Background: I occasionally use this kind of construct to be able to turn on/off debugging output on-the-fly: The compiler has to always read the value from memory, so changing that variable/memory during debugging should modify the control flow accordingly. The MSVC output does re-read the value but ignores it (presumably due to constant folding and/or dead code elimination), which of course defeats my intentions here.
Edits:
The elimination of the reads and writes to volatile is discussed here: Is it allowed for a compiler to optimize away a local volatile variable? (thanks Nathan!). I think the standard is abundantly clear that those reads and writes must happen. But that discussion does not cover whether it is legal for the compiler to take the results of those reads for granted and optimize based on that. I suppose this is under-/unspecified in the standard, but I'd be happy if someone proved me wrong.
I can of course make x a non-local variable to side-step the issue. This question is more out of curiosity.
I think [intro.execution] (paragraph number vary) could be used to explain MSVC behavior:
An instance of each object with automatic storage duration is associated with each entry into its block. Such an object exists and retains its last-stored value during the execution of the block and while the block is suspended...
The standard does not permit elimination of a read through a volatile glvalue, but the paragraph above could be interpreted as allowing to predict the value false.
BTW, the C Standard (N1570 6.2.4/2) says that
An object exists, has a constant address, and retains its last-stored value throughout its lifetime.34
34) In the case of a volatile object, the last store need not be explicit in the program.
It is unclear if there could be a non-explicit store into an object with automatic storage duration in C memory/object model.
TL;DR The compiler can do whatever it wants on each volatile access. But the documentation has to tell you.--"The semantics of an access through a volatile glvalue are implementation-defined."
The standard defines for a program permitted sequences of "volatile accesses" & other "observable behavior" (achieved via "side-effects") that an implementation must respect per "the 'as-if' rule".
But the standard says (my boldface emphasis):
Working Draft, Standard for Programming Language C++
Document Number: N4659
Date: 2017-03-21
§ 10.1.7.1 The cv-qualifiers
5 The semantics of an access through a volatile glvalue are implementation-defined. […]
Similarly for interactive devices (my boldface emphasis):
§ 4.6 Program execution
5 A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input. [...]
7 The least requirements on a conforming implementation are:
(7.1) — Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine.
(7.2) — At program termination, all data written into files shall be identical to one of the possible results that execution of the program according to the abstract semantics would have produced.
(7.3) — The input and output dynamics of interactive devices shall take place in such a fashion that prompting output is actually delivered before a program waits for input. What constitutes an interactive device is implementation-defined.
These collectively are referred to as the observable behavior of the program. [...]
(Anyway what specific code is generated for a program is not specified by the standard.)
So although the standard says that volatile accesses can't be elided from the abstract sequences of abstract machine side effects & consequent observable behaviors that some code (maybe) defines, you can't expect anything to be reflected in object code or real-world behaviour unless your compiler documentation tells you what constitutes a volatile access. Ditto for interactive devices.
If you are interested in volatile vis a vis the abstract sequences of abstract machine side effects and/or consequent observable behaviors that some code (maybe) defines then say so. But if you are interested in what corresponding object code is generated then you must interpret that in the context of your compiler & compilation.
Chronically people wrongly believe that for volatile accesses an abstract machine evaluation/read causes an implemented read & an abstract machine assignment/write causes an implemented write. There is no basis for this belief absent implementation documentation saying so. When/iff the implementation says that it actually does something upon a "volatile access", people are justified in expecting that something--maybe, the generation of certain object code.
I believe it is legal to skip the check.
The paragraph that everyone likes to quote
34) In the case of a volatile object, the last store need not be explicit in the program
does not imply that an implementation must assume such stores are possible at any time, or for any volatile variable. An implementation knows which stores are possible. For instance, it is entirely reasonable to assume that such implicit writes only happen for volatile variables that are mapped to device registers, and that such mapping is only possible for variables with external linkage. Or an implementation may assume that such writes only hapen to word-sized, word-aligned memory locations.
Having said that, I think MSVC behaviour is a bug. There is no real-world reason to optimise away the call. Such optimisation may be compliant, but it is needlessly evil.
As the title says:
What exactly is the "as-if" rule?
A typical answer one would get is:
The rule that allows any and all code transformations that do not change the observable behavior of the program
From time to time, we keep getting behaviors from certain implementations, which are attributed to this rule. Many times wrongly.
So, what exactly is this rule? The standard does not clearly mention this rule as a section or paragraph, so what exactly falls under the purview of this rule?
To me, it seems like a grey area which is not defined in detail by the standard. Can someone elaborate on the details, citing the references from the standard?
Note: Tagging this as C and C++ both, because it is relevant to both languages.
What is the "as-if" rule?
The "as-if" rule basically defines what transformations an implementation is allowed to perform on a legal C++ program. In short, all transformations that do not affect a program's "observable behavior" (see below for a precise definition) are allowed.
The goal is to give implementations freedom to perform optimizations as long as the behavior of the program remains compliant with the semantics specified by the C++ Standard in terms of an abstract machine.
Where does the Standard introduce this rule?
The C++11 Standard introduces the "as-if" rule in Paragraph 1.9/1:
The semantic descriptions in this International Standard define a parameterized nondeterministic abstract
machine. This International Standard places no requirement on the structure of conforming implementations.
In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming
implementations are required to emulate (only) the observable behavior of the abstract machine as explained
below.
Also, an explanatory footnote adds:
This provision is sometimes called the “as-if” rule, because an implementation is free to disregard any requirement of this
International Standard as long as the result is as if the requirement had been obeyed, as far as can be determined from the
observable behavior of the program. For instance, an actual implementation need not evaluate part of an expression if it can
deduce that its value is not used and that no side effects affecting the observable behavior of the program are produced.
What does the rule mandate exactly?
Paragraph 1.9/5 further specifies:
A conforming implementation executing a well-formed program shall produce the same observable behavior
as one of the possible executions of the corresponding instance of the abstract machine with the same program
and the same input. However, if any such execution contains an undefined operation, this International
Standard places no requirement on the implementation executing that program with that input (not even
with regard to operations preceding the first undefined operation).
It is worth stressing that this constraint applies when "executing a well-formed program" only, and that the possible outcomes of executing a program which contains undefined behavior are unconstrained. This is made explicit in Paragraph 1.9/4 as well:
Certain other operations are described in this International Standard as undefined (for example, the effect
of attempting to modify a const object). [ Note: This International Standard imposes no requirements on
the behavior of programs that contain undefined behavior. —end note ]
Finally, concerning the definition of "observable behavior", Paragraph 1.9/8 goes as follows:
The least requirements on a conforming implementation are:
— Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to one of the possible results that
execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place in such a fashion that prompting
output is actually delivered before a program waits for input. What constitutes an interactive device
is implementation-defined.
These collectively are referred to as the observable behavior of the program. [ Note: More stringent
correspondences between abstract and actual semantics may be defined by each implementation. —end
note ]
Are there situations where this rule does not apply?
To the best of my knowledge, the only exception to the "as-if" rule is copy/move elision, which is allowed even though the copy constructor, move constructor, or destructor of a class have side effects. The exact conditions for this are specified in Paragraph 12.8/31:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the constructor selected for the copy/move operation and/or the destructor for the object
have side effects. [...]
In C11 the rule is never called by that name. However C, just like C++, defines the behaviour in terms of abstract machine. The as-if rule is in C11 5.1.2.3p4 and p6:
In the abstract machine, all expressions are evaluated as specified by the semantics. 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 (including any caused by calling a function or accessing a volatile object).
[...]
The least requirements on a conforming implementation are:
Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
The input and output dynamics of interactive devices shall take
place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.
In C, C++, Ada, Java, SML... in any programming language well specified by describing the (usually many possible, non-deterministic) behavior(s) of a program (exposed to series of interactions on I/O ports), there is no distinct as-if rule.
An example of distinct rule is the one that says that a division by zero raises an exception (Ada, Caml) or a null dereference raises an exception (Java). You could change the rule to specify something else and you would end up with a different language (that some people would rather call a "dialect"(*). A distinct rule is there to specify some distinct uses of a programming language like a distinct grammatical rule cover some syntax constructs.
(*) A dialect according to some linguists is a language with an "army". in that context, that could mean a programming language without a committee and a specific industry of compiler editors.
The as-if rule is not a distinct rule; it doesn't cover any program in particular and is not even a rule that could be discussed, removed, or altered in any way: the so called "rule" simply reiterates that program semantics is defined, and can only be portably (universally) defined, in term of the visible interactions of an execution of the program with the "external" world.
The external world can be I/O interfaces (stdio), a GUI, even an interactive interpreter that output the resulting value of a pure applicative language. In C and C++ is includes the (vaguely specified) accesses to volatile objects, which is another way of saying that some objects at given point must be represented in memory strictly according to the ABI (Application Binary Interface) without ever mentioning the ABI explicitly.
The definition of what is a trace of execution, also called the visible or observable behavior defines what is meant by "as-if rule". The as-if rule tries to explain it, but by doing so, it confuses people more than it clarifies things as it gives the expression of being an additional semantic rule giving more leeway to the implementation.
Summary:
The so called "as-if rule" does not relax any constraints on implementations.
You cannot remove the as-if rule in any programming language specified in term of visible behavior (execution traces composed for interaction with the external world) to get a distinct dialect.
You cannot add the as-if rule to any programming language not specified in term of visible behavior.
As the title says:
What exactly is the "as-if" rule?
A typical answer one would get is:
The rule that allows any and all code transformations that do not change the observable behavior of the program
From time to time, we keep getting behaviors from certain implementations, which are attributed to this rule. Many times wrongly.
So, what exactly is this rule? The standard does not clearly mention this rule as a section or paragraph, so what exactly falls under the purview of this rule?
To me, it seems like a grey area which is not defined in detail by the standard. Can someone elaborate on the details, citing the references from the standard?
Note: Tagging this as C and C++ both, because it is relevant to both languages.
What is the "as-if" rule?
The "as-if" rule basically defines what transformations an implementation is allowed to perform on a legal C++ program. In short, all transformations that do not affect a program's "observable behavior" (see below for a precise definition) are allowed.
The goal is to give implementations freedom to perform optimizations as long as the behavior of the program remains compliant with the semantics specified by the C++ Standard in terms of an abstract machine.
Where does the Standard introduce this rule?
The C++11 Standard introduces the "as-if" rule in Paragraph 1.9/1:
The semantic descriptions in this International Standard define a parameterized nondeterministic abstract
machine. This International Standard places no requirement on the structure of conforming implementations.
In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming
implementations are required to emulate (only) the observable behavior of the abstract machine as explained
below.
Also, an explanatory footnote adds:
This provision is sometimes called the “as-if” rule, because an implementation is free to disregard any requirement of this
International Standard as long as the result is as if the requirement had been obeyed, as far as can be determined from the
observable behavior of the program. For instance, an actual implementation need not evaluate part of an expression if it can
deduce that its value is not used and that no side effects affecting the observable behavior of the program are produced.
What does the rule mandate exactly?
Paragraph 1.9/5 further specifies:
A conforming implementation executing a well-formed program shall produce the same observable behavior
as one of the possible executions of the corresponding instance of the abstract machine with the same program
and the same input. However, if any such execution contains an undefined operation, this International
Standard places no requirement on the implementation executing that program with that input (not even
with regard to operations preceding the first undefined operation).
It is worth stressing that this constraint applies when "executing a well-formed program" only, and that the possible outcomes of executing a program which contains undefined behavior are unconstrained. This is made explicit in Paragraph 1.9/4 as well:
Certain other operations are described in this International Standard as undefined (for example, the effect
of attempting to modify a const object). [ Note: This International Standard imposes no requirements on
the behavior of programs that contain undefined behavior. —end note ]
Finally, concerning the definition of "observable behavior", Paragraph 1.9/8 goes as follows:
The least requirements on a conforming implementation are:
— Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to one of the possible results that
execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place in such a fashion that prompting
output is actually delivered before a program waits for input. What constitutes an interactive device
is implementation-defined.
These collectively are referred to as the observable behavior of the program. [ Note: More stringent
correspondences between abstract and actual semantics may be defined by each implementation. —end
note ]
Are there situations where this rule does not apply?
To the best of my knowledge, the only exception to the "as-if" rule is copy/move elision, which is allowed even though the copy constructor, move constructor, or destructor of a class have side effects. The exact conditions for this are specified in Paragraph 12.8/31:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the constructor selected for the copy/move operation and/or the destructor for the object
have side effects. [...]
In C11 the rule is never called by that name. However C, just like C++, defines the behaviour in terms of abstract machine. The as-if rule is in C11 5.1.2.3p4 and p6:
In the abstract machine, all expressions are evaluated as specified by the semantics. 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 (including any caused by calling a function or accessing a volatile object).
[...]
The least requirements on a conforming implementation are:
Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
The input and output dynamics of interactive devices shall take
place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.
In C, C++, Ada, Java, SML... in any programming language well specified by describing the (usually many possible, non-deterministic) behavior(s) of a program (exposed to series of interactions on I/O ports), there is no distinct as-if rule.
An example of distinct rule is the one that says that a division by zero raises an exception (Ada, Caml) or a null dereference raises an exception (Java). You could change the rule to specify something else and you would end up with a different language (that some people would rather call a "dialect"(*). A distinct rule is there to specify some distinct uses of a programming language like a distinct grammatical rule cover some syntax constructs.
(*) A dialect according to some linguists is a language with an "army". in that context, that could mean a programming language without a committee and a specific industry of compiler editors.
The as-if rule is not a distinct rule; it doesn't cover any program in particular and is not even a rule that could be discussed, removed, or altered in any way: the so called "rule" simply reiterates that program semantics is defined, and can only be portably (universally) defined, in term of the visible interactions of an execution of the program with the "external" world.
The external world can be I/O interfaces (stdio), a GUI, even an interactive interpreter that output the resulting value of a pure applicative language. In C and C++ is includes the (vaguely specified) accesses to volatile objects, which is another way of saying that some objects at given point must be represented in memory strictly according to the ABI (Application Binary Interface) without ever mentioning the ABI explicitly.
The definition of what is a trace of execution, also called the visible or observable behavior defines what is meant by "as-if rule". The as-if rule tries to explain it, but by doing so, it confuses people more than it clarifies things as it gives the expression of being an additional semantic rule giving more leeway to the implementation.
Summary:
The so called "as-if rule" does not relax any constraints on implementations.
You cannot remove the as-if rule in any programming language specified in term of visible behavior (execution traces composed for interaction with the external world) to get a distinct dialect.
You cannot add the as-if rule to any programming language not specified in term of visible behavior.
In many discussions about undefined behavior (UB), the point of view has been put forward that in the mere presence in a program of any construct that has UB in a program mandates a conforming implementation to do just anything (including nothing at all). My question is whether this should be taken in that sense even in those cases where the UB is associated to the execution of code, while the behaviour (otherwise) specified in the standard stipulates that the code in question should not be executed (and this possibly for specific input to the program; it might not be decidable at compile time).
Phrased more informally, does the smell of UB mandate a conforming implementation to decide that the whole program stinks, and refuse to execute correctly even the parts of the program for which the behaviour is perfectly well defined. An example program would be
#include <iostream>
int main()
{
int n = 0;
if (false)
n=n++; // Undefined behaviour if it gets executed, which it doesn't
std::cout << "Hi there.\n";
}
For clarity,
I am assuming the program is well-formed (so in particular the UB is not associated to preprocessing). In fact I am willing to restrict to UB associated to "evaluations", which clearly are not compile-time entities. The definitions pertinent to the example given are, I think,(emphasis is mine):
Sequenced before is an asymmetric, transitive, pair-wise relation between evaluations executed by a single thread (1.10), which induces a partial order among those evaluations
The value computations of the operands of an
operator are sequenced before the value computation of the result of the operator. If a side effect on a scalar object is unsequenced relative to either ... or a value computation using the value of the same scalar object, the behavior is undefined.
It is implicitly clear that the subjects in the final sentence, "side effect" and "value computation", are instances of "evaluation", since that is what the relation "sequenced before" is defined for.
I posit that in the above program, the standard stipulates that no evaluations occur for which the condition in the final sentence is satisfied (unsequenced relative to each other and of the described kind) and that therfore the program does not have UB; it is not erroneous.
In other words I am convinced that the answer to the question of my title is negative. However I would appreciate the (motivated) opinions of other people on this matter.
Maybe an additional question for those who advocate an affirmative answer, would that mandate that the proverbial reformatting of your hard drive might occur when an erroneous program is compiled?
Some related pointers on this site:
Observable behavior and undefined behavior -- What happens if I don't call a destructor?
Comments to this answer https://stackoverflow.com/a/24143792/1436796 (I do no longer stand absolutely with my answer itself)
C++ What is the earliest undefined behavior can manifest itself?
Difference between Undefined Behavior and Ill-formed, no diagnostic message required and its two answers, which represent opposite points of view
If a side effect on a scalar object is unsequenced relative to etc
Side effects are changes in the state of the execution environment (1.9/12). A change is a change, not an expression that, if evaluated, would potentially produce a change. If there is no change, there is no side effect. If there is no side effect, then no side effect is unsequenced relative to anything else.
This does not mean that any code which is never executed is UB-free (though I'm pretty sure most of it is). Each occurrence of UB in the standard needs to be examined separately. (The stricken-out text is probably overly cautious; see below).
The standard also says that
A conforming implementation executing a well-formed program shall produce the same observable behavior
as one of the possible executions of the corresponding instance of the abstract machine with the same program
and the same input. However, if any such execution contains an undefined operation, this International
Standard places no requirement on the implementation executing that program with that input (not even
with regard to operations preceding the first undefined operation).
(emphasis mine)
This, as far as I can tell, is the only normative reference that says what the phrase "undefined behavior" means: an undefined operation in a program execution. No execution, no UB.
No. Example:
struct T {
void f() { }
};
int main() {
T *t = nullptr;
if (t) {
t->f(); // UB if t == nullptr but since the code tested against that
}
}
Deciding whether a program will perform an integer division by 0 (which is UB) is in general equivalent the halting problem. There is no way a compiler can determine that, in general. And so the mere presence of possible UB can not logically affect the rest of the program: a requirement to that effect in the standard, would require each compiler vendor to provide a halting problem solver in the compiler.
Even simpler, the following program has UB only if the user inputs 0:
#include <iostream>
using namespace std;
auto main() -> int
{
int x;
if( cin >> x ) cout << 100/x << endl;
}
It would be absurd to maintain that this program in itself has UB.
Once the undefined behavior occurs, however, then anything can happen: the further execution of code in the program is then compromised (e.g. the stack might have been fouled up).
In the general case the best we can say here is that it depends.
One case where the answer is no, happens when dealing with indeterminate values. The latest draft clearly makes it undefined behavior to produce an indeterminate value during an evaluation with some exceptions but the code sample clearly shows how subtle it could be:
[ Example:
int f(bool b) {
unsigned char c;
unsigned char d = c; // OK, d has an indeterminate value
int e = d; // undefined behavior
return b ? d : 0; // undefined behavior if b is true
}
— end example ]
so this line of code:
return b ? d : 0;
is only undefined if b is true. This seems to be the intuitive approach and seems to be how John Regehr sees it as well, if we read It’s Time to Get Serious About Exploiting Undefined Behavior.
In this case the answer is yes, the code is erroneous even though we are not calling the code invoking undefined behavior:
constexpr const char *str = "Hello World" ;
constexpr char access()
{
return str[100] ;
}
int main()
{
}
clang chooses to make access erroneous even though it is never invoked (see it live).
There's a clear divide between inherent undefined behaviour, such as n=n++, and code that can have defined or undefined behaviour depending on the program state at runtime, such as x/y for ints. In the latter case the program is required to work unless y is 0, but in the first case the compiler's asked to generate code that's totally illegitimate - it's within its rights to refuse to compile, it may just not be "bullet proofed" against such code and consequently its optimiser state (register allocations, records of which values may have been modified since read etc) gets corrupted resulting in bogus machine code for that and surrounding source code. It may be that early analysis recognised an "a=b++" situation and generated code for the preceding if to jump over a two byte instruction, but when n=n++ is encountered no instruction was output, such that the if statement jumps somewhere into the following opcodes. Anyway, it's simply game over. Putting an "if" in front, or even wrapping it in a different function, isn't documented as "containing" the undefined behaviour... bits of code aren't tainted with undefined behaviour - the Standard consistently says "the program has undefined behaviour".
It should be, if not "shall".
Behavior, by definition from ISO C (no corresponding definition found in ISO C++ but it should be still somehow applicable), is:
3.4
1 behavior
external appearance or action
And UB:
WG21/N4527
1.3.25 [defns.undefined]
undefined behavior
behavior for which this International Standard imposes no requirements [ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed.
—end note ]
Despite "to behaving during translation" above, the word "behavior" used by ISO C++ is mainly about the execution of programs.
WG21/N4527
1.9 Program execution [intro.execution]
1 The semantic descriptions in this International Standard define a parameterized nondeterministic abstract machine. This International Standard places no requirement on the structure of conforming implementations. In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below.5
2 Certain aspects and operations of the abstract machine are described in this International Standard as implementation-defined (for example, sizeof(int)). These constitute the parameters of the abstract machine.
Each implementation shall include documentation describing its characteristics and behavior in these respects.6 Such documentation shall define the instance of the abstract machine that corresponds to that implementation (referred to as the “corresponding instance” below).
3 Certain other aspects and operations of the abstract machine are described in this International Standard as unspecified (for example, evaluation of expressions in a new-initializer if the allocation function fails to allocate memory (5.3.4)). Where possible, this International Standard defines a set of allowable behaviors.
These define the nondeterministic aspects of the abstract machine. An instance of the abstract machine can thus have more than one possible execution for a given program and a given input.
4 Certain other operations are described in this International Standard as undefined (for example, the effect of attempting to modify a const object). [ Note: This International Standard imposes no requirements on the behavior of programs that contain undefined behavior. —end note ]
5 A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input. However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation).
5) This provision is sometimes called the “as-if” rule, because an implementation is free to disregard any requirement of this International Standard as long as the result is as if the requirement had been obeyed, as far as can be determined from the observable behavior of the program. For instance, an actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no side effects affecting the observable behavior of the program are produced.
6) This documentation also includes conditionally-supported constructs and locale-specific behavior. See 1.4.
It is clear the undefined behavior would be caused by specific language construct used wrongly or in a non-portable way (which is not conforming to the standard). However, the standard mention nothing about which specific portion of code in a program would cause it. In other words, "having undefined behavior" is the property (about conforming) of the whole program being executed, not any smaller parts of it.
The standard could have given a stronger guarantee to make the behavior well-defined once some specific code is not being executed, only when there exists a way to map the C++ code to the corresponding behavior precisely. This is hard (if not impossible) without a detailed semantic model about execution. In short, the operational semantics given by the abstract machine model above is not enough to achieve the stronger guarantee. But anyway, ISO C++ would never be JVMS or ECMA-335. And I don't expect there would be a complete set of formal semantics describing the language.
A key problem here is the meaning of "execution". Some people think "executing a program" means making the program being run. This is not quite true. Note the representation of program executed in the abstract machine is not specified. (Also note "this International Standard places no requirement on the structure of conforming implementations".) The code being executed here can be literally C++ code (not necessarily machine code or some other forms of intermediate code which is not specified by the standard at all). This effectively allows the core language to be implemented as an interpreter, an online partial evaluator or some other monsters translating C++ code on-the-fly. As a result, actually there is no way to split the phases of translation (defined by ISO C++ [lex.phases]) completely ahead of the process of execution without knowledge about specific implementations. Thus, it is necessary to allow UB occurring during the translation when it is too difficult to specify portable well-defined behavior.
Besides the problems above, perhaps for most ordinary users, one (non-technical) reason is enough: it is simply unnecessary to provide the stronger guarantee, allow bad code and defeat one of the (probable most important) usefulness aspect of UB itself: to encourage quickly throwing away some (unnecessarily) nonportable smelly code without effort to "fix" them which would be eventually in vain.
Additional notes:
Some words are copied and reconstructed from one of my reply to this comment.
A C compiler is allowed to do anything it likes as soon as a program enters a state via which there is no defined sequence of events which would allow the program to avoid invoking Undefined Behavior at some point in the future (note any loop which does not have any side-effects, and which does not have an exit condition which a compiler would be to required to recognize, invokes Undefined Behavior in and of itself). The compiler's behavior in such cases is bound by the laws of neither time nor causality. In situations where Undefined Behavior occurs in an expression whose result is never used, some compilers won't generate any code for the expression (so it will never "execute") but that won't prevent compilers from using the Undefined Behavior to make other inferences about program behavior.
For example:
void maybe_launch_missiles(void)
{
if (should_launch_missiles())
{
arm_missiles();
if (should_launch_missiles())
launch_missiles();
}
disarm_missiles();
}
int foo(int x)
{
maybe_launch_missiles();
return x<<1;
}
Under the C current C standard, if the compiler could determinate that disarm_missiles() would always return without terminating but the three other external functions called above might terminate, the most efficient standard-compliant replacement for the statement foo(-1); (return value ignored) would be should_launch_missiles(); arm_missiles(); should_launch_missiles(); launch_missiles();.
Program behavior will only be defined if either call to should_launch_missiles() terminates without returning, if the first call returns non-zero and arm_missiles() terminates without returning, or if both calls return non-zero and launch_missiles() terminates without returning. A program which works correctly in those cases will abide by the standard regardless of what it does in any other situation. If returning from maybe_launch_missiles() would cause Undefined Behavior, compiler would not be required to recognize the possibility that either call to should_launch_missiles() could return zero.
As a consequence, some modern compilers, the effect of left-shifting a negative number may be worse than anything that could be caused by any kind of Undefined Behavior on a typical C99 compiler on platforms that separate code and data spaces and trap stack overflow. Even if code engaged in Undefined Behavior which could cause random control transfers, there would be no means by which it could cause arm_missiles() and launch_missiles() to be called consecutively without having an intervening call to disarm_missiles() unless at least one call to should_launch_missiles() returned a non-zero value. A hyper-modern compiler, however, may negate such protections.
In the dialect processed by gcc with full optimizations enabled, if a program contains two constructs which would behave identically in cases where both are defined, reliable program operation requires that any code that would switch among them only be executed in cases where both are defined. For example, when optimizations are enabled, both ARM gcc 9.2.1 and x86-64 gcc 10.1 will process the following source:
#include <limits.h>
#if LONG_MAX == 0x7FFFFFFF
typedef int longish;
#else
typedef long long longish;
#endif
long test(long *x, long *y)
{
if (*x)
{
if (x==y)
*y = 1;
else
*(longish*)y = 1;
}
return *x;
}
into machine code that will test if x and y are equal, set *x to 1 if they are and *y to 1 if they aren't, but return the previous value of *x in either case. For purpose of determining whether anything might affect *x, gcc decides that both branches of the if are equivalent, and thus only evaluates the "false" branch. Since that can't affect *x, it concludes that the if as a whole can't either. That determination is unswayed by its observation that on the true branch, the write to *y can be replaced with a write to *x.
In the context of a safety-critical embedded system, the posted code would be considered defective:
The code should not pass code review and/or standards compliance (MISRA, etc)
Static analysis (lint, cppcheck, etc) should flag this as a defect
Some compilers can flag this as a warning (implying a defect, as well.)
As the title says:
What exactly is the "as-if" rule?
A typical answer one would get is:
The rule that allows any and all code transformations that do not change the observable behavior of the program
From time to time, we keep getting behaviors from certain implementations, which are attributed to this rule. Many times wrongly.
So, what exactly is this rule? The standard does not clearly mention this rule as a section or paragraph, so what exactly falls under the purview of this rule?
To me, it seems like a grey area which is not defined in detail by the standard. Can someone elaborate on the details, citing the references from the standard?
Note: Tagging this as C and C++ both, because it is relevant to both languages.
What is the "as-if" rule?
The "as-if" rule basically defines what transformations an implementation is allowed to perform on a legal C++ program. In short, all transformations that do not affect a program's "observable behavior" (see below for a precise definition) are allowed.
The goal is to give implementations freedom to perform optimizations as long as the behavior of the program remains compliant with the semantics specified by the C++ Standard in terms of an abstract machine.
Where does the Standard introduce this rule?
The C++11 Standard introduces the "as-if" rule in Paragraph 1.9/1:
The semantic descriptions in this International Standard define a parameterized nondeterministic abstract
machine. This International Standard places no requirement on the structure of conforming implementations.
In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming
implementations are required to emulate (only) the observable behavior of the abstract machine as explained
below.
Also, an explanatory footnote adds:
This provision is sometimes called the “as-if” rule, because an implementation is free to disregard any requirement of this
International Standard as long as the result is as if the requirement had been obeyed, as far as can be determined from the
observable behavior of the program. For instance, an actual implementation need not evaluate part of an expression if it can
deduce that its value is not used and that no side effects affecting the observable behavior of the program are produced.
What does the rule mandate exactly?
Paragraph 1.9/5 further specifies:
A conforming implementation executing a well-formed program shall produce the same observable behavior
as one of the possible executions of the corresponding instance of the abstract machine with the same program
and the same input. However, if any such execution contains an undefined operation, this International
Standard places no requirement on the implementation executing that program with that input (not even
with regard to operations preceding the first undefined operation).
It is worth stressing that this constraint applies when "executing a well-formed program" only, and that the possible outcomes of executing a program which contains undefined behavior are unconstrained. This is made explicit in Paragraph 1.9/4 as well:
Certain other operations are described in this International Standard as undefined (for example, the effect
of attempting to modify a const object). [ Note: This International Standard imposes no requirements on
the behavior of programs that contain undefined behavior. —end note ]
Finally, concerning the definition of "observable behavior", Paragraph 1.9/8 goes as follows:
The least requirements on a conforming implementation are:
— Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to one of the possible results that
execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place in such a fashion that prompting
output is actually delivered before a program waits for input. What constitutes an interactive device
is implementation-defined.
These collectively are referred to as the observable behavior of the program. [ Note: More stringent
correspondences between abstract and actual semantics may be defined by each implementation. —end
note ]
Are there situations where this rule does not apply?
To the best of my knowledge, the only exception to the "as-if" rule is copy/move elision, which is allowed even though the copy constructor, move constructor, or destructor of a class have side effects. The exact conditions for this are specified in Paragraph 12.8/31:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the constructor selected for the copy/move operation and/or the destructor for the object
have side effects. [...]
In C11 the rule is never called by that name. However C, just like C++, defines the behaviour in terms of abstract machine. The as-if rule is in C11 5.1.2.3p4 and p6:
In the abstract machine, all expressions are evaluated as specified by the semantics. 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 (including any caused by calling a function or accessing a volatile object).
[...]
The least requirements on a conforming implementation are:
Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
The input and output dynamics of interactive devices shall take
place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.
In C, C++, Ada, Java, SML... in any programming language well specified by describing the (usually many possible, non-deterministic) behavior(s) of a program (exposed to series of interactions on I/O ports), there is no distinct as-if rule.
An example of distinct rule is the one that says that a division by zero raises an exception (Ada, Caml) or a null dereference raises an exception (Java). You could change the rule to specify something else and you would end up with a different language (that some people would rather call a "dialect"(*). A distinct rule is there to specify some distinct uses of a programming language like a distinct grammatical rule cover some syntax constructs.
(*) A dialect according to some linguists is a language with an "army". in that context, that could mean a programming language without a committee and a specific industry of compiler editors.
The as-if rule is not a distinct rule; it doesn't cover any program in particular and is not even a rule that could be discussed, removed, or altered in any way: the so called "rule" simply reiterates that program semantics is defined, and can only be portably (universally) defined, in term of the visible interactions of an execution of the program with the "external" world.
The external world can be I/O interfaces (stdio), a GUI, even an interactive interpreter that output the resulting value of a pure applicative language. In C and C++ is includes the (vaguely specified) accesses to volatile objects, which is another way of saying that some objects at given point must be represented in memory strictly according to the ABI (Application Binary Interface) without ever mentioning the ABI explicitly.
The definition of what is a trace of execution, also called the visible or observable behavior defines what is meant by "as-if rule". The as-if rule tries to explain it, but by doing so, it confuses people more than it clarifies things as it gives the expression of being an additional semantic rule giving more leeway to the implementation.
Summary:
The so called "as-if rule" does not relax any constraints on implementations.
You cannot remove the as-if rule in any programming language specified in term of visible behavior (execution traces composed for interaction with the external world) to get a distinct dialect.
You cannot add the as-if rule to any programming language not specified in term of visible behavior.