这个 C++ 应该吗?临时绑定参考成员是非法的吗?

发布于 2024-08-05 07:44:36 字数 3141 浏览 4 评论 0 原文

我的问题(在此之后,抱歉介绍太长,问题在粗体中)最初受到 Herb Sutters Exceptional C++ 中第 23 条的启发,其中我们找到这样的东西:
<剪>


...
int main()
{
  GenericTableAlgorithm a( "Customer", MyWorker() );
  a.Process();
}


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 ...
};


现在,我的代码存在以下问题(不,我绝不怀疑 Sutter 先生作为 C++ 专家的能力):

    1. 这样的例子是行不通的,因为 GTAClient& worker 是一个非常量引用,不能临时使用,但是,它可能是预先标准编写的或拼写错误,无论如何,这不是我的观点。
    2. 让我想知道的是,即使问题 1. 被忽略,他将如何处理工人参考。
      显然,其目的是让MyWorker在通过GTAClient(多态)接口访问的GenericTableAlgorithm的NVI中使用;这排除了实现拥有 GTAClient 类型的(值)成员,因为这会导致切片等。值语义与多态性不能很好地混合。
      它不能具有 MyWorker 类型的数据成员,因为 GenericTableAlgorithm 不知道该类。
      所以我得出结论,它一定是通过指针或引用使用的,保留了原始对象和多态性。
    3. 由于指向临时对象的指针 (MyWorker()) 很少是一个好主意,我假设作者的计划是使用绑定到(const)引用的临时对象的延长生命周期,并存储对象 pimpl_ 中的此类引用指向并从那里使用它。 (注意:GTAClient 中也没有克隆成员函数,这可以使这项工作正常进行;我们不要假设有一个基于 RTTI-typeinfo 的工厂潜伏在后台。
      在这里(最后!)我的问题是:(如何)可以合法地将临时对象传递给类的引用成员并延长生命周期


    §12.2.5 中的标准(C++0x 版本,但在 C++ 中是相同的,不知道章节号)对生命周期扩展做出了以下例外: “-在构造函数的构造函数初始化程序 (12.6.2) 中,临时绑定到引用 成员 一直持续到构造函数退出为止。”

    因此,该对象不能在客户端代码 a.Process(); 的调用中使用,因为 MyWorker() 引用的临时对象已经失效!

    现在考虑我自己制作的一个示例来演示该问题(在 GCC4.2 上测试):

    #include ;
    使用 std::cout; 
    使用 std::endl;
    
    结构体oogie {
     〜oogie()
     {
      计算<< “~oogie():”<<这个<< “:”<< m_i <<结束;
     }
     oogie(int i_)
      : m_i(i_)
     {
      计算<< “oogie():”<<这个<< “:”<< m_i <<结束;
     }
    
     void call() 常量
     {
      计算<< “调用():” <<这个<< “:”<< m_i <<结束;
     }
     int m_i;
    };
    
    oogie 函数(int i_=100)
    {
     返回 oogie(i_);
    }
    
    结构体kangoo 
    {
     kangoo(const oogie& o_)
     : m_o(o_)
     {
     }
    
     const oogie& m_o;
    };
    
    int main(int c_, char ** v_)
    {
    
     //按预期工作
     const oogie&参考 = 函数(400);
     //kablewy机器
     kangoo s(func(1000));
    
     计算<<参考.m_i <<结束;
    
     //kangoo 引用的 oogie 已经消失了
     计算<< s.m_o.m_i <<结束;
    
     //好的,ref还活着
     ref.call();
     //调用无效对象
     s.m_o.call();
    
     返回0;
    }
    

    产生输出

    oogie():0x7fff5fbff780:400
    oogie():0x7fff5fbff770:1000
    ~oogie():0x7fff5fbff770:1000
    400
    1000
    呼叫():0x7fff5fbff780:400
    调用():0x7fff5fbff770:1000
    〜oogie():0x7fff5fbff780:400
    
    
    

    您可以看到,在 const oogie& 的情况下ref func() 的立即绑定到引用的临时返回值具有所述引用的延长生命周期(直到 main 结束),因此没问题。
    但是: 1000-oogie 对象在 kango-s 构建完成后就已经被销毁了。代码有效,但我们在这里处理的是一个不死物体......

    所以再次提出问题:
    首先,我是否在这里遗漏了一些东西,并且代码是正确/合法的?
    其次,为什么即使指定了 -Wall,GCC 也不给我任何警告?应该吗?可以吗?

    感谢您的宝贵时间,
    马丁

  • 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

  • 如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

    扫码二维码加入Web技术交流群

    发布评论

    需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

    评论(4

    余厌 2024-08-12 07:44:36

    我认为这是一个棘手的部分,不太清楚。就在几天前,也有类似的问题。

    默认情况下,当创建临时对象的完整表达式完成时,它们将以与构造相反的顺序销毁。到目前为止,一切都很好并且可以理解,但是随后出现了异常(12.2 [class.temporary]/4,5),事情变得令人困惑。

    我不会处理标准中的确切措辞和定义,而是从工程/编译器的角度来处理问题。当函数完成时,堆栈帧被释放(堆栈指针移回到函数调用开始之前的原始位置),在堆栈中创建临时对象。

    这意味着临时对象永远无法在创建它的函数中存活下来。更准确地说,它无法在定义它的范围内生存,即使它实际上可以在创建它的完整表达式中生存。

    标准中的任何异常都不会超出此限制,在所有情况下,临时变量的生命周期都会延长到保证不超过创建临时变量的函数调用的程度。

    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.

    两个我 2024-08-12 07:44:36

    本周大师文章的原始版本位于:本周大师#15

    您的部分答案可能来自 Herb 本人,位于 “最重要 const 的候选者” - 将临时值分配给引用的“const”部分确实很重要。

    因此,这看起来好像是原始文章中的一个错误。

    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.

    忆悲凉 2024-08-12 07:44:36

    我始终认为传递地址参数(无论是通过引用还是指针)需要了解所传递事物的生命周期。时期。讨论结束。这似乎只是非垃圾收集/非引用管理环境的一个属性。

    这是GC的好处之一。

    在 C++ 中,有时生命周期问题可以通过以下方式解决:

    X::clone

    或通过接口的显式文档,例如:

    “由 Y 的实例化器来确保参数 x 中传递的 X 实例在整个生命周期中保持存在Y”

    或通过

    在接收者的内部显式复制x。

    那只是c++。 C++ 添加了对 const 引用的保证很好(而且确实有些必要),但仅此而已。

    因此,我认为问题中所示的代码是错误的,并且我认为它应该是:

    int main()
    {
      MyWorker w;
      GenericTableAlgorithm a( "Customer", w);
      a.Process();
    }
    

    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();
    }
    
    两人的回忆 2024-08-12 07:44:36

    我认为当前的 C++ 无法做到这一点。您需要移动语义,该语义将在C++x0。

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

    ~没有更多了~
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文