Should this C++ temporary binding to reference mem

2019-04-08 09:08发布

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):

    1. The example like that will not work, since GTAClient& worker is a non-const reference which can't take a temporary, but well, it might have been written pre-standard or a typo, whatever, that's not my point.
    2. What makes me wonder is what he is going to do with the worker reference, even if Problem 1. is ignored.
      Obviously the intention is to have 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.
      It cannot have a data member of type MyWorker either since that class is unknown to GenericTableAlgorithm.
      So I conclude it must have been meant to be used via pointer or reference, preserving the original object and polymorphic nature.
    3. Since pointers to temporary objects (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.)
      And here (finally!) my question sets in:(How) can passing a temporary to to a class' reference member with extended life-time be done legally ?


    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

  • 4条回答
    smile是对你的礼貌
    2楼-- · 2019-04-08 09:23

    The original Guru of the Week article is here: Guru of the Week #15.

    Part of your answer might come from Herb himself, in "A candidate for the most important const" - the "const" part of assigning a temporary to a reference is indeed important.

    So it would appear as if this is a bug in the original article.

    查看更多
    够拽才男人
    3楼-- · 2019-04-08 09:24

    I don't think you can do this with current C++. You need the move semantics that will be introduced in C++x0.

    查看更多
    Summer. ? 凉城
    4楼-- · 2019-04-08 09:31

    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.

    查看更多
    做自己的国王
    5楼-- · 2019-04-08 09:34

    I always considered that passing address parameters (whether by reference or pointer) required an understanding about the lifetime of the thing passed. Period. End of Discussion. This seems just an attribute of an un-garbage-collected/NOT-reference-managed environment.

    This is one of the benefits of GC.

    In c++ sometimes the lifetime issues were solved by:

    X::clone

    or by explicit documentation of the interface eg:

    "it is up to the instantiator of Y to ensure that the instance of X passed in parameter x remains in existence for the entire lifetime of Y"

    or by

    explicit copying of x in the guts of the receiver.

    That's just c++. That c++ added a guarantee on const references was nice (and really somewhat necessary) but that was that.

    Thus I consider the code as shown in the question to be wrong and am of the opinion it should have been:

    int main()
    {
      MyWorker w;
      GenericTableAlgorithm a( "Customer", w);
      a.Process();
    }
    
    查看更多
    登录 后发表回答