Is the following code valid C++, according to the standard (discounting the ...s)?
bool f(T& r)
{
if(...)
{
r = ...;
return true;
}
return false;
}
T x = (f(x) ? x : T());
It is known to compile in the GCC versions this project uses (4.1.2 and 3.2.3... don't even get me started...), but should it?
Edit: I added some details, for example as to how f() conceptually looks like in the original code. Basically, it's meant to be initialize x in certain conditions.
Syntactically it is, however if you try this
it outputs some random junk. However, if you modify
then it outputs 10. In the first case, the object
x
is declared, and the compiler assigns some pseudo-arbitrary value (so you do not initialize it), whereas in the second you specifically assign a value (T()
, i.e.0
) after the declaration, i.e. you initialize it.I think your question is similar to this one: Using newly declared variable in initialization (int x = x+1)?
Although scope plays a role the real issue is about object lifetime and more exactly for object with non-trivial initialization when does the lifetime begin.
This is closely related to Can initializing expression use the variable itself? and Is passing a C++ object into its own constructor legal?. Although my answers to those questions do not neatly answer this question, so it does not seem like a duplicate.
The key portion of the draft C++ standard we are concerned with here is section
3.8
[basic.life] which says:So in this case we satisfy the first bullet, storage has been obtained.
The second bullet is where we find trouble:
Non-trivial initialization case
We can get a base reasoning from defect report 363 which asks:
and the proposed resolution was:
So before the lifetime of an object begins we are limited in what we can do with an object. We can see from the defect report binding a reference to
x
is valid as long as it binds directly.What we can do is covered in section
3.8
(The same section and paragraph the defect report quotes) says (emphasis mine):In your case we are accessing a non-static data member here, see emphasis above:
So if
T
has non-trivial initialization then this line invokes undefined behavior and so would reading fromr
which would also be an access, covered in defect report 1531.If
x
has static storage duration it will be zero-initialized but as far as I can tell this does not count as it's initialization is complete since the constructor would be called during dynamic initialization.Trivial Initialization case
If
T
has trivial initializaton then the lifetime begins once storage is obtained and writing tor
is well defined behavior. Although note that readingr
before it has initialized will invoke undefined behavior since it would produce an indeterminate value. Ifx
has static storage duration then it is zero-initialized and we don't have this issue.Should it compile, in either cases whether you are invoking undefined behavior or not this allowed to compile. The compiler is not obligated to produce a diagnostic for undefined behavior although it may. It is only obligated to produce a diagnostic for ill-formed code which none of the troublesome cases here are.
It undoubtedly should compile, but may conditionally lead to undefined behavior.
T
is a non-primitive type, undefined behavior if it is assigned.T
is a primitive type, well-defined behavior if it is non-local, and undefined behavior if it is not assigned before reading (except for character types, where it is defined to give an unspecified value).The relevant part of the Standard is this rule from 3.8, Object lifetime:
So the lifetime of
x
hasn't started yet. In the same section, we find the rule that governs usingx
:If your type is non-primitive, then trying to assign it is actually a call to
T::operator=
, a non-static member function. Full-stop, that is undefined behavior according to case 2.Primitive types are assigned without invoking a member function, so let's now take a closer look at section 4.1, Lvalue-to-rvalue conversion, to see when exactly that lvalue-to-rvalue conversion will be undefined behavior:
(note that these rules reflect a rewrite for the upcoming C++14 standard in order to make them easier to understand, but I don't think there's an actual change in the behavior here)
Your variable
x
has1 an indeterminate value at the time an lvalue-reference is made and passed tof()
. As long as that variable has primitive type and its value is assigned before it is read (a read is lvalue-to-rvalue conversion), the code is fine.If the variable isn't assigned before being read, the effect depends on
T
. Character types will cause code that executes and uses an arbitrary but legal character value. All other types cause undefined behavior.1 Unless
x
has static storage duration, for example a global variable. In that case it is zero-initialized before execution, according to section 3.6.2 Initialization of non-local variables:In this case of static storage duration it is not possible to run into lvalue-to-rvalue conversion of an unspecified value. But zero-initialization is not a valid state for all types, so still be careful of that.