Temporary Objects - When Are They Created, How to Recognise Them in Code

Temporary objects - when are they created, how do you recognise them in code?

I wasn't entirely satisfied by the answers, so I took a look at:

"More Effective C++", Scott Meyers. Item 19: "Understand the origin of
temporary Objects"

. Regarding Bruce Eckel's coverage of "Temporaries",
well, as I suspect and as Christian Rau directly points out, it's plain
wrong! Grrr! He's (Eckel's) using us as guinea pigs!! (it would be a
good book for newbies like me once he corrects all his mistakes)

Meyer: "True temporary objects in C++ are invisible - they don't
appear in your source code. They arise whenever a non-heap object is
created but not named. Such unnamed objects usually arise in one of
two situations: when implicit type conversions are applied to make
function calls succeed and when functions return objects."

"Consider first the case in which temporary objects are created to
make function calls succeed. This happens when the type of object
passed to a function is not the same as the type of the parameter to
which it is being bound."

"These conversions occur only when passing objects by value or when
passing to a reference-to-const parameter. They do not occur when
passing an object to a reference-to-non-const parameter."

"The second set of circumstances under which temporary objects are
created is when a function returns an object."

"Anytime you see a reference-to-const parameter, the possibility
exists that a temporary will be created to bind to that parameter.
Anytime you see a function returning an object, a temporary will be
created (and later destroyed)."

The other part of the answer is found in: "Meyer: Effective C++", in
the "Introduction":

"a copy constructor is used to initialize an object with a different
object of the same type:"

String s1;       // call default constructor
String s2(s1); // call copy constructor
String s3 = s2; // call copy constructor

"Probably the most important use of the copy constructor is to define
what it means to pass and return objects by value."

Regarding my questions:

f5() = X(1) //what is happening?

Here a new object isn't being initialized, ergo this is not
initialization(copy constructor): it's an assignment (as
Matthieu M pointed out).

The temporaries are created because as per Meyer (top paragraphs),
both functions return values, so temporary objects are being created.
As Matthieu pointed out using pseudo-code, it becomes:
__0.operator=(__1) and a bitwise copy takes place(done by the
compiler).

Regarding:

void f7(X& x);
f7(f5);

ergo, a temporary cannot be created (Meyer: top paragraphs).
If it had been declared: void f7(const X& x); then a temporary would
have been created.

Regarding a temporary object being a constant:

Meyer says it (and Matthieu): "a temporary will be created to bind to that
parameter."

So a temporary is only bound to a constant reference and is itself not
a "const" object.

Regarding:
what is X(1)?

Meyer, Item27, Effective C++ - 3e, he says:

"C-style casts look like this: (T)expression //cast expression to be
of type T

Function-style casts use this syntax: T(expression) //cast expression
to be of type T"

So X(1) is a function-style cast. 1 the expression is being cast to
type X.

And Meyer says it again:

"About the only time I use an old-style cast is when I want to call an
explicit constructor to pass an object to a function. For example:

class Widget {
public:
explicit Widget(int size);
...
};

void doSomeWork(const Widget& w);
doSomeWork(Widget(15)); //create Widget from int
//with function-style cast

doSomeWork(static_cast<Widget>(15));

Somehow, deliberate object creation doesn't "feel" like a cast, so I'd
probably use the function-style cast instead of the static_cast in
this case."

Temporary objects in C++

When you say f(a + b), the parameter of f needs to bind to the value with which the function was called, and since that value is an rvalue (being the value of a function call with non-reference return type)*, the parameter type must be a const-lvalue-reference, an rvalue-reference or a non-reference.

By constrast, when you say (a + b).g(), the temporary object is used as the implicit instance argument in the member function call, which does not care about the value category. Mutable values bind to non-const and const member functions, and const values only bind to const member functions (and similarly for volatile).

Actually, C++11 did add a way to qualify the value category of the implicit instance argument, like so:

struct Foo()
{
Foo operator+(Foo const & lhs, Foo const & rhs);

void g() &; // #1, instance must be an lvalue
void g() &&; // #2, instance must be an rvalue
}

Foo a, b;

a.g(); // calls #1
b.g(); // calls #1
(a + b).g(); // calls #2

*) this is the case for an overloaded operator as in this example, and also for built-in binary operators. You can of course make overloaded operators which produce lvalues, though going against the common conventions would probably be considered very confusing.

Disallowing creation of the temporary objects

Edit: As j_random_hacker notes, it is possible to force the user to declare a named object in order to take out a lock.

However, even if creation of temporaries was somehow banned for your class, then the user could make a similar mistake:

// take out a lock:
if (m_multiThreaded)
{
CSingleLock c(&m_criticalSection, TRUE);
}

// do other stuff, assuming lock is held

Ultimately, the user has to understand the impact of a line of code that they write. In this case, they have to know that they're creating an object and they have to know how long it lasts.

Another likely mistake:

 CSingleLock *c = new CSingleLock(&m_criticalSection, TRUE);

// do other stuff, don't call delete on c...

Which would lead you to ask "Is there any way I can stop the user of my class from allocating it on the heap"? To which the answer would be the same.

In C++0x there will be another way to do all this, by using lambdas. Define a function:

template <class TLock, class TLockedOperation>
void WithLock(TLock *lock, const TLockedOperation &op)
{
CSingleLock c(lock, TRUE);
op();
}

That function captures the correct usage of CSingleLock. Now let users do this:

WithLock(&m_criticalSection, 
[&] {
// do stuff, lock is held in this context.
});

This is much harder for the user to screw up. The syntax looks weird at first, but [&] followed by a code block means "Define a function that takes no args, and if I refer to anything by name and it is the name of something outside (e.g. a local variable in the containing function) let me access it by non-const reference, so I can modify it.)

How to create temporary object in C++

This is another instance of the "everything that can be a declaration is a declaration" rule. [stmt.ambig]/p1:

There is an ambiguity in the grammar involving expression-statements
and declarations: An expression-statement with a function-style
explicit type conversion (5.2.3) as its leftmost subexpression can be
indistinguishable from a declaration where the first declarator
starts with a (. In those cases the statement is a declaration.

The standard provides the following examples:

Assuming T is a simple-type-specifier,

T(a)->m = 7;       // expression-statement
T(a)++; // expression-statement
T(a,5)<<c; // expression-statement
T(*d)(int); // declaration
T(e)[5]; // declaration
T(f) = { 1, 2 }; // declaration
T(*g)(double(3)); // declaration

In the last example above, g, which is a pointer to T, is initialized
to double(3). This is of course ill-formed for semantic reasons, but
that does not affect the syntactic analysis.

and also:

class T {
// ...
public:
T();
T(int);
T(int, int);
};

T(a); // declaration
T(*b)(); // declaration
T(c)=7; // declaration
T(d),e,f=3; // declaration
extern int h;
T(g)(h,2); // declaration

The simplest way to disambiguate is probably an extra set of parentheses. (A(a)); is unambiguously an expression-statement.

When is 'delete' called on temporary objects created with `new`?

I don't know the details of your SmartPtr class.

In any case, if you have a constructor like this:

SmartPtr::SmartPtr(Pointee * p):_pointee(new Pointee(*p))
{}

and this is the destructor:

SmartPtr::~SmartPtr()
{
delete _pointee;
}

then with this code:

SmartPtr pTime0(new Time(0,0,1));

you leak one instance of Time(0,0,1).

In fact, you have one more new than delete (2 news and 1 delete):

Step #1: You call new Time(0,0,1) and create a new object on the heap.

(new count == 1)

Step #2: You pass this pointer to SmartPtr constructor, which deep copies previously created object and allocates a new copy on the heap, and keeps track of this copy via its _pointee data member.

(new count == 2)

Step #3: When the SmartPtr destructor runs, it deletes the instance pointed by _pointee data member, but you leaked the firts Time(...) created on the heap with new Time(0,0,1).

(delete count == 1; new count == 2)

A possible fix for that could be to just have this constructor:

SmartPtr::SmartPtr(Pointee * p)
: _pointee(p) // <--- transfer ownerhsip (no deep copies) !
{}

An easy way to identify potential leaks in these cases is to put some console tracing output in Time class constructors and destructor, and check that the trace output of destructor matches the ones of constructors, e.g.:

Time::Time(....)
{
// Do construction work....

std::cout << "Time constructor\n";
}

Time::~Time(....)
{
// Do destructor work....

std::cout << "Time destructor\n";
}

The total count of "Time constructor" strings should match the total count of "Time destructor" strings.

Lifetime extension of temporary objects: what is the full expression containing a function call?

Yes, the whole std::copy_if call is the full-expression and the temporary std::vector<Stuff> will be destroyed only after the call returns.

This is different from by-value function parameters. If the constructor took a std::vector<Stuff> instead of a const std::vector<Stuff>&, then it would be implementation-defined whether the object lives that long or is destroyed immediately when the constructor returns.


A full-expression is one of the expressions listed in [intro.execution]/5. None of the specific cases (such as unevaluated operands, immediate invocations, etc.) apply here and so falling through, the relevant condition is:

an expression that is not a subexpression of another expression and that is not otherwise part of a full-expression.

The copy_if call expression is the only one in that statement to which this applies (outside of the lambda body).



Related Topics



Leave a reply



Submit