0

Is it UB to return a struct without initializing it, if the only subsequent use is in an initialization statement as shown below:

typedef struct { int x; } s;

s callee(void) {
  s ret;
  return ret;
}

void caller() {
  s dummy = callee();
}
BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • You seem to think it matters whether or not the `struct` is unused; what about circumstances where the compiler cannot determine this? – Scott Hunter Jun 08 '18 at 00:08
  • @ScottHunter - I don't know if it matters that the struct is unused, that's why I am asking. Obviously there are many cases where the compiler cannot determine this, such as if `callee` and `caller` were in separate compilation units - but I'm not sure what you are getting at here. I'm not asking "Is it UB to return an uninit struct if the compiler can _prove_ it is unused" - I'm asking if the above pattern is UB. For example, maybe the `return s` is UB (regardless of caller) or maybe the `s dummy = callee()` is UB, or maybe not. – BeeOnRope Jun 08 '18 at 00:10
  • 1
    Note: whether something is UB or not is a standalone attribute of the source, and does _not_ depend on what a compiler can or cannot prove, or what compiler you use, or even whether any compilers exist at all. – BeeOnRope Jun 08 '18 at 00:14
  • 2
    In C++, this is undefined behavior. C++ allows making copies of indeterminate values of narrow character type (creating new indeterminate values) but for any other type, an lvalue to rvalue conversion (which includes returning by value) produces undefined behavior. – Ben Voigt Jun 08 '18 at 00:18
  • @BeeOnRope: It would be possible for there not to be any UB yet, but the return value cannot be used in any way without triggering UB. That's not what happens (at least in C++) but it reasonable to consider it. This does happen in other circumstances, for example returning a dangling reference. – Ben Voigt Jun 08 '18 at 00:19
  • @BenVoigt so would the above be defined in C++ if the struct contained a `char x` instead of an `int x`, or it cannot be a `struct` at all? – BeeOnRope Jun 08 '18 at 00:20
  • @BeeOnRope: Interesting question. I am fairly sure an aggregate of nothing but `char` would result in indeterminate values and avoid UB (because the behavior of the struct defaulted copy constructor is defined as copying the individual elements, so it is undefined behavior if and only if one of the element copies is). – Ben Voigt Jun 08 '18 at 00:23
  • Thanks @BenVoigt for the notes on C++. In my case I'm not making any further use of the value (the program terminates immediately after `caller` returns. I am asking [in this context](https://stackoverflow.com/q/50664325/149138) where the `make()` function seems to return an uninitialized value, and if that were UB it could be used to argue that the observed compilation of this program is allowed due to the unconditional UB (you can get around it by calling `exit()` before the `return`, but you need a two-compilation unit program in that case). – BeeOnRope Jun 08 '18 at 00:26
  • This is also UB in C99: "If a return statement with an expression is executed, **the value of the expression** is returned to the caller as the value of the function call expression." and "The behavior is undefined in the following circumstances... The value of an object with automatic storage duration is used while it is indeterminate" – Ben Voigt Jun 08 '18 at 00:27
  • In your other question, the return statement is not executed (`sink` does not return because `exit` does not return) and therefore the UB doesn't happen. supercat is rarely wrong though, perhaps the UB he's talking about is something different. – Ben Voigt Jun 08 '18 at 00:33
  • @BenVoigt - correct, I organized it that way so the UB doesn't happen, but it requires splitting the reproduction case into two files, `sink.c` and `main.c` (because if the compiler sees the `exit(0)` it compiles stuff entirely differently). I wanted a simpler one-file repro, thus this question. I don't think the discussion with supercat on the comments on M.M's answer are relevant here: it's a digression talking about whether this is allowed in C++, but everyone seems to agree that in C, at least, it's a mis-compilation. – BeeOnRope Jun 08 '18 at 00:36
  • @BenVoigt - perhaps it would be better to comment over there, but doesn't RVO allow the storage for `f1` and `f2` to overlap? Also, it seems that in C11, there is a exception to the quote above for the case where the address of the auto-duration variable is taken, which happens over in the other question, but not here, so maybe this question wasn't in-fact a good match to the other one. – BeeOnRope Jun 08 '18 at 00:38
  • NRVO requires more research, but M.M.'s claim was based on the object's lifetime not starting yet, and that argument is wrong. "Before the lifetime of an object has started ..., any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 15.7. Otherwise, such a pointer refers to allocated storage (6.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined." – Ben Voigt Jun 08 '18 at 00:41
  • Comments aren't for extended discussion - if someone is interested in C++ then post a new question – M.M Jun 08 '18 at 01:37
  • The duplicate would be [(Why) is using an uninitialized variable undefined behavior?](https://stackoverflow.com/questions/11962457/why-is-using-an-uninitialized-variable-undefined-behavior). See the accepted answer or the one posted by yours sincerely. Structs are no different, except that the struct itself cannot be a trap representation (individual members can). – Lundin Jun 08 '18 at 06:48
  • @Lundin I think the fact that this includes returning the indeterminate value makes it not be a duplicate of questions that only involve assignment – M.M Jun 08 '18 at 08:51
  • @M.M No it is not an exact duplicate (or I would have close-voted). There's a nuance here because the OP is returning a whole struct, and there's the special exception for structs and trap representations that you address in your answer. Apart from that, the answer is otherwise the same: UB since the address of the automatic variable was not taken. – Lundin Jun 08 '18 at 09:42
  • @BenVoigt in C++ "_an lvalue to rvalue conversion_" where do you see one in the example? – curiousguy Jun 09 '18 at 18:27

2 Answers2

2

Consider this similar code first:

s ret;
s dummy = ret;

A struct cannot have a trap representation (C11 6.2.6.1/6). But this code causes undefined behaviour due to the Itanium Clause (C11 6.3.2.1/2) which says that using the value of an uninitialized automatic object that never has its address taken causes UB.

So this code would be well-defined:

s ret;
&ret;
s dummy = ret;

For further reading on that clause see: Is a^a or a-a undefined behaviour if a is not initialized?.


For the version with a function return value: it's not spelled out in the standard whether the return value counts as an automatic object for the purposes of the Itanium Clause. I would tend to say it does not, since the return value is not described as an object by the Standard. But it would be good if someone familiar with the Itanium ABI could comment on whether passing an uninitialized struct through a return value triggers a NaT exception.

In lieu of that, my position is that the function call version has the same semantics as the assignment version discussed above, i.e. the posted code is UB but adding &ret; makes it well-defined.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • 1
    Related, see [Is using a structure without all members assigned undefined?](https://stackoverflow.com/q/47433041/608639) – jww Jun 08 '18 at 01:52
  • 1
    Out of curiousity, what makes you call 6.3.2.1/2 "the Itanium Clause"? Something to do with a particular Intel device, I take it? – Lundin Jun 08 '18 at 06:42
  • @Lundin yes, [see here](http://blog.frama-c.com/index.php?post/2013/03/13/indeterminate-undefined) – M.M Jun 08 '18 at 08:50
  • Note that all compilers I have tested treat the stronger rule in J.2 ("the behavior is undefined if ... a value is used while it is indeterminate") as 100% normative, even though it appears in an informative annex and, as far as I can tell, the normative text does not go that far. It is my considered opinion that the stronger rule does actually reflect the intent of the committee. (The difference is that taking the address doesn't make any difference by itself, nor does it matter whether the type has trap representations.) – zwol Jun 08 '18 at 11:33
  • 1
    @Lundin: On the Itanium, each 64-bit register could have 2^64+1 states: it could either hold one of 2^64 values, or it could hold a special "not-a-value" state. The Itanium would generate a trap if code tried to e.g. perform arithmetic on a register when it was in a "not a value" state, and for some reason people seem to think that's the reason why using an uninitialized automatic variable invokes UB. – supercat Jun 08 '18 at 16:24
  • @supercat the "some reason" being that this clause was inserted into C11 so that the Itanium behaviour would be conforming. (Since in C99 without traps, `int x; int y = x - x;` was well-defined and yielded `y` having an indeterminate value). – M.M Jun 08 '18 at 23:11
  • @M.M: The clause would also be needed to justify the behavior of compilers on many other platforms which substantially predate the Itanium. On platforms where register operations are evaluated using a type larger than "char", an uninitialized "unsigned char" kept in a register might have a value outside the range 0..CHAR_MAX. Attempts to use that value could result in weird behaviors even given straightforward code generation. For example, given `extern volatile unsigned char vv; void test(int mode) { unsigned char x; if (!mode) x=vv; vv = x>>4; }` it would not be possible... – supercat Jun 09 '18 at 20:00
  • ...for `vv` to get written with a value outside the range 0..15 if `x` merely held an Unspecified value. On the other hand, the most efficient way of implementing the above on many platforms would allocate a larger-than-8-bit register to `x`, and--if `mode` isn't zero, store whatever happened to be in bits 4..11 of that register into `vv`. Under C89, that behavior wouldn't have been conforming (though many compilers likely behaved that way anyhow) but C99 changed that. – supercat Jun 09 '18 at 20:07
  • @M.M: BTW, a much narrower change which would have accomplished the same thing would be to recognize that objects of automatic duration whose address is not taken may be stored in containers with more bits than the in-memory representation, and may thus have trap representations or padding bits even when the memory representation does not. Such a scenario would allow for the Itanium, as well as the other implementations I described, but would usefully constrain behavior on platforms which document the ways in which objects may be stored in registers. – supercat Jun 09 '18 at 20:26
-1

TLDR: While the ability for functions to "pass through" indeterminate values that callers end up ignoring offers benefits which on most platforms far exceed the cost, and should thus be provided by quality implementations targeting such platforms, the Standard does not require implementations to provide it, and thus "clever" implementations do not.


There are many constructs which all compilers would have handled consistently when the Standard was written, but which the Standard does not explicitly define. The Standard notes that a common way of processing actions where the Standard would impose no requirements is "in a documented manner characteristic of the environment" but in the rationale notes that the decision of when to do so is Quality-of-Implementation issue rather than a conformance one. The rationale also recognizes that an implementation may be conforming while being of such poor quality as to be useless, but that the authors do not think it necessary to expend effort forbidding such implementations.

There are two sensible and useful ways a quality compiler for a typical platform could process code like the above:

  1. A compiler could trap in an implementation-defined means if a function tries to return an uninitialized object, without regard for whether the calling code would make use of the value [the code to generate the trap may have no way of knowing anything about the calling code].

  2. A compiler could leave some arbitrary collection of bits someplace which the caller may or may not use, with no side-effect if the caller doesn't actually use them. Note that if objects of a certain type wouldn't have padding bits when stored in memory, they may have padding bits when stored in registers, and might behave oddly if those padding bits aren't set correctly.

The authors of the Standard made no attempt to enumerate all of the things an implementation might do when dealing with indeterminate values, since they figured that implementers seeking to produce quality implementations would decide what course of action would be most appropriate given the intended platforms and purposes of the implementations in question.

Unfortunately, even though on any common platform it would cost essentially nothing to let functions safely return indeterminate values in cases where the callers are going to ignore them, and in some cases doing that would yield more efficient code than would otherwise be possible (e.g. saying that given:

extern volatile int vv1, vv2, vv3;
int foo(int mode)
{
  int result;
  vv1 = 1;
  if (mode & 1)
    result = vv2;
  if (mode & 2)
    result = vv3;
  return result;
}

the statement "foo(0);" would store 1 to vv1 with no other side-effects would let avoid the need to have programmers force compilers to generate an unnecessary load) it has become fashionable for compiler writers to find "clever" ways of exploiting the fact that the Standard doesn't require such guarantees. For example, the above code might be "optimized" to:

int foo(int mode)
{
  vv1 = 1;
  if (!(mode & 2))
    return vv2;
  if (mode & 1)
    +vv2; // Access and ignore value
  return vv3;
}

Whether or not the practical value of such "optimizations" would ever exceed that of letting programmers allow compilers avoid unnecessary loads, programmers who can't be sure their code will only be run on quality implementations will need to make allowances for "clever" ones.

supercat
  • 77,689
  • 9
  • 166
  • 211