If Temporaries Are Implicitly Non-Modifiable, How Does This Work

If temporaries are implicitly non-modifiable, how does this work?

I'm told that, in C++03, temporaries are implicitly non-modifiable.

That is not correct. Temporaries are created, among other circumstances, by evaluating rvalues, and there are both non-const rvalues and const rvalues. The value category of an expression and the constness of the object it denotes are mostly orthogonal 1. Observe:

      std::string foo();
const std::string bar();

Given the above function declarations, the expression foo() is a non-const rvalue whose evaluation creates a non-const temporary, and bar() is a const rvalue that creates a const temporary.

Note that you can call any member function on a non-const rvalue, allowing you to modify the object:

foo().append(" was created by foo")   // okay, modifying a non-const temporary
bar().append(" was created by bar") // error, modifying a const temporary

Since operator= is a member function, you can even assign to non-const rvalues:

std::string("hello") = "world";

This should be enough evidence to convince you that temporaries are not implicitly const.

1: An exception are scalar rvalues such as 42. They are always non-const.

Lifetime of implicitly casted temporaries

.#1 calls boost::tuple<const double&>::tuple(const double&). In order to do this, a temporary double is created by the full-expression foo(boost::tuple<const double&>(2)). Then a temporary boost::tuple<const double&> is created. It has a reference member which is bound to the temporary double. Both temporaries exist until full-expression #1 is done, and is still valid when foo is called.

.#2 calls boost::tuple<const double&>::tuple(const boost::tuple<int>&). This expression creates a temporary boost::tuple<int>. The lifetime of that temporary is similarly not a problem. But consider what happens when that tuple constructor is called. Simplified / pseudocode classes:

template<> class tuple<int> {
private:
int member1_;
//...
};

template<> class tuple<const double&> {
private:
const double& member1_;
public:
tuple(const tuple<int>& int_tup) : member1_(int_tup.member1_) {}
// ...
};

The mem-initializer member1(int_tup.member1_) converts the int value to a temporary double and binds that double to the class reference member. This temporary double is created by the full-expression member1_(int_tup.member1_), not by the full-expression foo(boost::make_tuple(2)). A special exception for mem-initializers guarantees that the temporary double is okay until the end of the constructor in which it was created, but then there's no guarantee it's still valid when foo is called.

So the important difference is that statement #1 creates the temporary double itself, but statement #2 indirectly causes a temporary double to be created within another function. Exactly which full-expression creates a temporary has an impact on how long that temporary will live.

Am I right in saying that const_cast followed by modification on a ref-to-const bound to a temporary is okay?

No.

First, as far as I can tell, whether it is a literal or not is
irrelevant. Rvalues of non-class types always have non-cv qualified
types (§3.10/9), however, in §8.5.3 (initialization of a reference), we
have:

A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:

[...]

--

Otherwise, a temporary of type “cv1 T1” is created and initialized from the initializer expression using the rules for a non-reference copy initialization (8.5). The reference is then bound to the temporary. If T1 is reference-related to T2, cv1 must be the same cv-qualification as, or greater cvqualification
than, cv2; otherwise, the program is ill-formed.

(All of the preceding points concern either lvalues or class types.)

In our case, we have:

int const& x = ...;

So cv1 T1 is int const, and the temporary object we create has type
int const. This is a top level const (on the object), so any attempt
to modify it is undefined behavior.

At least, that's my interpretation. I wish the standard were a bit clearer about this.

Allocation and immutability of references to rvalues in C++

It doesn't matter where they're allocated. Somewhere in memory on your computer, or perhaps not at all if they're optimised out. Don't worry about it.

They're not immutable — you could modify them if you like (try std::string().append('!')); it's just that you're only allowed to bind a temporary to a reference if it's a reference-to-const. And if you don't bind it to a reference, you can't actually name it later to do anything with it.

This limitation was intended to prevent programmer mistakes, because modifying a temporary is usually pointless, though Visual Studio relaxes that restriction. However, you can still make use of this lifetime-extension without const by using an rvalue reference:

std::string&& ref = std::string();
ref.append('!');
std::cout << ref << '\n';

Why would you do it? Who knows.

As for your final question (one question per question please next time), no, those two references refer to two, unrelated, temporary std::string objects.

What are C++ temporaries?

In C++ temporaries are unnamed objects that compiler creates in various contexts. The typical uses include reference initialization, argument passing, evaluation of expressions (including standard type conversions), function returns, and exceptions (throw expressions).

As from this link:

When a temporary object is created to initialize a reference variable, the name of the temporary object has the same scope as that of the reference variable. When a temporary object is created during the evaluation of a full-expression (an expression that is not a subexpression of another expression), it is destroyed as the last step in its evaluation that lexically contains the point where it was created.

There are exceptions in the destruction of full-expressions:

  1. The expression appears as an initializer for a declaration defining an object: the temporary object is destroyed when the initialization is complete.
  2. A reference is bound to a temporary object: the temporary object is destroyed at the end of the reference's lifetime.

If a temporary object is created for a class with constructors, the compiler calls the appropriate (matching) constructor to create the temporary object.

When a temporary object is destroyed and a destructor exists, the compiler calls the destructor to destroy the temporary object. When you exit from the scope in which the temporary object was created, it is destroyed. If a reference is bound to a temporary object, the temporary object is destroyed when the reference passes out of scope unless it is destroyed earlier by a break in the flow of control. For example, a temporary object created by a constructor initializer for a reference member is destroyed on leaving the constructor.

In cases where such temporary objects are redundant, the compiler does not construct them, in order to create more efficient optimized code. This behavior could be a consideration when you are debugging your programs, especially for memory problems.

Let us summarize it this way. These temporary objects can be created for the following reasons:

  1. To initialize a const reference with an initializer of a type different from that of the underlying type of the reference being initialized.

  2. To store the return value of a function that returns a user-defined type. These temporaries are created only if your program does not copy the return value to an object. Because the return value is not copied to another object, a temporary object is created. A more common case where temporaries are created is during the evaluation of an expression where overloaded operator functions must be called. These overloaded operator functions return a user-defined type that often is not copied to another object.

  3. To store the result of a cast to a user-defined type. When an object of a given type is explicitly converted to a user-defined type, that new object is constructed as a temporary object.

Let's consider the example:

class X {
/ / ...
public:
/ / ...
X(int);
X(const X&);
~X();
};

X f(X);

void g()
{
X a(1);
X b = f(X(2));
a = f(a);
}

Here, an implementation might use a temporary in which to construct X(2) before passing it to f() using X’s copy-constructor; alternatively, X(2) might be constructed in the space used to hold the argument. Also, a temporary might be used to hold the result of f(X(2)) before copying it to b using X’s copy-constructor; alternatively, f()’s result might be constructed in b. On the other hand, the expression a=f(a) requires a temporary for the result of f(a), which is then assigned to a.



Related Topics



Leave a reply



Submit