Does holding a C++ reference prevents variable of being destroyed?

Viewed 598

Coming from a Java/C# background I am trying to implement kinda a null safety check object. The following code tries to check if there is indeed a color on the object. If there is, I want be able to do some stuff (like drawing the color on a shape). I want to be able to set this color back to null (noneColor), preventing for coloring the object.

I didn't want to set this color has a pointer, because it would be necessary to delete it on every setter and on the deconstructor. Is this a bad thought?

Code:

static const Color& noneColor = Color();

static bool isNoneColor(const Color& color) {
    return std::addressof(noneColor) == std::addressof(color);
}

MyShape:MyShape() {
    this->color = noneColor; // this->color is an object of Color
}

void MyShape::setColor(const Color& newColor)
{
    this->color= newColor;
}

void MyShape::draw()
{
    if(!isNoneColor(this->color))
        // Color shape
}

//different scope/class
void B::setColorForShape()
{
    Color color = Color(255,255,255);
    a->setColor(color);
    // Calling isNoneColor(a->color); should return false;
    // After this function ends does variable a->color have undefined behavior? 
}

void B::setNoneColorForShape()
{
    a->setColor(noneColor);
    //Calling isNoneColor(a->color); should return true;
}

Is it safe to call isNoneColor with a->color afterwards the method ends?

Edit:

MyShape:color is declared has Color& color. I want to be able to set a noneColor state on my object MyShape. With a simple Color object I can't know if that color is "null"/empty state.

3 Answers

Given that

MyShape::color is declared as a Color&

... the code is a bit problematic.

The thing to know about C++ references is that you can't re-seat them -- that is, unlike a C++ pointer, once a C++ reference is declared, it will always reference the object it was declared to reference, and cannot be re-directed to reference some other object instead. (Btw this is also the reason why you're not allowed to declare an unbound reference, e.g. MyColor & c; won't compile because it would be useless)

From that, it follows that what this code actually does:

void MyShape::setColor(const Color& newColor)
{
   this->color= newColor;
}

is overwrite the contents of (whatever Color object this->color is referencing) with the contents of newColor). It doesn't change the reference itself at all; rather it writes through the reference. (Remember that references are defined as aliases-to-something, so wherever you specify the reference, the program will behave as if you had specified the object-the-reference-is-referencing there instead)

To answer the titular question: holding a C++ reference does not guarantee that the object the reference is referencing will not be destroyed. It's quite possible to delete (or otherwise destroy) the referenced item, and end up with a "dangling reference" that will invoke Undefined Behavior if you try to use it.

As general advice, if you want to have a guard-value to represent "no-color", the best way to do it is to represent the guard-value as an actual color object (e.g. Color(0,0,0,0) could work, if your Color class has an alpha-channel, since any color with alpha=0 is transparent, hence no-color). That way there is no need to play games with pointers or references to try to represent that information. If that's not doable for some reason, have a look at std::optional as a way to represent "either-a-value-or-nothing".

As a final note, using pointers does not mean you have to use new and delete -- pointers work equally well (and you can shoot yourself in the foot with them equally badly) when they are pointing to stack-objects or member-objects. Which is to say, pointers are very powerful, and also very dangerous, and I wouldn't recommend using them for this purpose.

The answer to your question

Does holding a C++ reference prevents variable of being destroyed?

is: not generally, but in this case, yes. The standard has this to say

  1. There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. The first context is [..]
  2. The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:
    • A temporary bound to a reference member in a constructor’s ctor-initializer(12.6.2) persists until the constructor exits.
    • A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.
    • The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
    • A temporary bound to a reference in a new-initializer(5.3.4) persists until the completion of the full-expression containing the new-initializer. [Example][Note]

(from 12.2 [class.temporary], emphasis added, example and note omitted)


Although this is a bad practice, and something like T const& t = T(); will look odd to many C++ programmers (I presume), it is technically legal. It's usefulness can be debated, as T const t = T(); will do the same job without looking odd. See Jeremy Friesner's answer for why your approach will not work as expected because references cannot be re-seated.

Does holding a C++ reference prevents variable of being destroyed?

In general no, not in the way Java references prevent variables from being destroyed.

The lifetime of an object is determined by their storage class: Lifetime of automatic object ends when executions goes out of the block where the variable was declared, lifetime of dynamic object ends when it is deleted, lifetime of static object ends when the process terminates. Lifetime of temporary object ends at the end of the full-expression where it was created, except in special case of binding temporary to a reference, in which case the lifetime of the temporary object ends when the lifetime of the reference ends. And only that one reference; No other reference affects the lifetime of the said temporary object. This is useful in generic programming because it allows certain objects to be treated as if they were references.

isNoneColor can never return true for a member of MyShape because such member will always have different address than noneColor.

Edit: Above was written with the reasonable assumption that the member is an object of type Color

Edit:

MyShape:color is declared has Color& color.

In this case, the member cannot refer to noneColor because a reference to non-const cannot be bound to the lvalue referred by a reference to const... Unless you use const_cast, but that would be naughty.

Related