My question (which will follow after this, sorry about the long intro, the question is down there in bold) is originally inspired by Item 23 in Herb Sutters Exceptional C++ where we find something like this:
<snip>
...
int main()
{
GenericTableAlgorithm a( "Customer", MyWorker() );
a.Process();
}
with
class GenericTableAlgorithm
{
public:
GenericTableAlgorithm( const string& table,
GTAClient& worker );
bool Process();
private:
struct GenericTableAlgorithmImpl* pimpl_; //implementation
};
class GTAClient
{
///...
virtual bool ProcessRow( const PrimaryKey& ) =0;
//...
};
class MyWorker : public GTAClient
{
// ... override Filter() and ProcessRow() to
// implement a specific operation ...
};
</snip>
Now, I have the following problems with that code (and no, I in no way doubt Mr. Sutter's prowess as a C++ expert):
MyWorker
used in the NVI of GenericTableAlgorithm
accessed by GTAClient
(polymorphic) interface; this rules out that the implementation owns a (value)member of type GTAClient
, since that would cause slicing etc. value-semantics don't mix well with polymorphism.
MyWorker
either since that class is unknown to GenericTableAlgorithm
.
MyWorker()
) are rarely a good idea, i assume the author's plan was to use the extended life-time of temporaries bound to (const) references, and store such a reference in the object pimpl_
points to and use it from there. (Note: there is also no clone-member function in GTAClient, which could have made this work; let's not assume there is a RTTI-typeinfo-based Factory lurking in the background.)
The standard in §12.2.5(the C++0x version but it's the same in C++, don't know about the chapter number) makes the following exception from lifetime extension:
"-A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits."
Therefore the object cannot be used in the call of the client code a.Process(); because the referenced temporary from MyWorker()
is already dead!
Consider now an example of my own crafting that demonstrates the problem (tested on GCC4.2):
#include <iostream>
using std::cout;
using std::endl;
struct oogie {
~oogie()
{
cout << "~oogie():" << this << ":" << m_i << endl;
}
oogie(int i_)
: m_i(i_)
{
cout << "oogie():" << this << ":" << m_i << endl;
}
void call() const
{
cout << "call(): " << this << ":" << m_i << endl;
}
int m_i;
};
oogie func(int i_=100)
{
return oogie(i_);
}
struct kangoo
{
kangoo(const oogie& o_)
: m_o(o_)
{
}
const oogie& m_o;
};
int main(int c_, char ** v_)
{
//works as intended
const oogie& ref = func(400);
//kablewy machine
kangoo s(func(1000));
cout << ref.m_i << endl;
//kangoo's referenced oogie is already gone
cout << s.m_o.m_i << endl;
//OK, ref still alive
ref.call();
//call on invalid object
s.m_o.call();
return 0;
}
which produces the output
oogie():0x7fff5fbff780:400 oogie():0x7fff5fbff770:1000 ~oogie():0x7fff5fbff770:1000 400 1000 call(): 0x7fff5fbff780:400 call(): 0x7fff5fbff770:1000 ~oogie():0x7fff5fbff780:400
You can see that in the case of const oogie& ref the immediately bound-to-reference temporary return value of func() has the extended lifetime of said reference (until end of main), so it's OK.
BUT: The 1000-oogie object is already destroyed right after kangoo-s was constructed. The code works, but we are dealing with an undead object here...
So to pose the question again:
Firstly, Am I missing something here and the code is correct/legal?.
Secondly, why does GCC give me no warning of this, even with -Wall specified? Should it? Could it?
Thanks for your time,
martin
I think this is a tricky part that is not too clear. There was a similar question just a couple of days ago.
By default temporaries are destroyed in reverse order of construction when the full-expression in which they were created completes. Up to here everything is fine and understood, but then exceptions arise (12.2 [class.temporary]/4,5) and things become confusing.
Instead of dealing with the exact wording and definitions in the standard I will approach the problem from an engineering / compiler perspective. Temporaries are created in the stack, when a function completes the stack frame is freed (the stack pointer is moved back to the original position before the function call started).
This implies that a temporary can never survive the function in which it was created. More exactly, it cannot survive the scope where it was defined, even if it can in fact survive the full-expression in which it was created.
None of the exceptions in the standard falls out of this restriction, in all cases the lifetime of the temporary is extended to a point that is guaranteed not to exceed the function call in which the temporary was created.