Aggregate reference member and temporary lifetime

前端 未结 1 1722
感动是毒
感动是毒 2020-12-29 09:36

Given this code sample, what are the rules regarding the lifetime of the temporary string being passed to S.

struct S
{
    // [1] S(const std::         


        
相关标签:
1条回答
  • 2020-12-29 10:14

    The lifetime of temporary objects bound to references is extended, unless there's a specific exception. That is, if there is no such exception, then the lifetime will be extended.

    From a fairly recent draft, N4567:

    The second context [where the lifetime is extended] is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

    • (5.1) A temporary object bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.
    • (5.2) The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
    • (5.3) A temporary bound to a reference in a new-initializer (5.3.4) persists until the completion of the full-expression containing the new-initializer.

    The only significant change to C++11 is, as the OP mentioned, that in C++11 there was an additional exception for data members of reference types (from N3337):

    • A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

    This was removed in CWG 1696 (post-C++14), and binding temporary objects to reference data members via the mem-initializer is now ill-formed.


    Regarding the examples in the OP:

    struct S
    {
        const std::string& str_;
    };
    
    S a{"foo"}; // direct-initialization
    

    This creates a temporary std::string and initializes the str_ data member with it. The S a{"foo"} uses aggregate-initialization, so no mem-initializer is involved. None of the exceptions for lifetime extensions apply, therefore the lifetime of that temporary is extended to the lifetime of the reference data member str_.


    auto b = S{"bar"}; // copy-initialization with rvalue
    

    Prior to mandatory copy elision with C++17: Formally, we create a temporary std::string, initialize a temporary S by binding the temporary std::string to the str_ reference member. Then, we move that temporary S into b. This will "copy" the reference, which will not extend the lifetime of the std::string temporary. However, implementations will elide the move from the temporary S to b. This must not affect the lifetime of the temporary std::string though. You can observe this in the following program:

    #include <iostream>
    
    #define PRINT_FUNC() { std::cout << __PRETTY_FUNCTION__ << "\n"; }
    
    struct loud
    {
        loud() PRINT_FUNC()
        loud(loud const&) PRINT_FUNC()
        loud(loud&&) PRINT_FUNC()
        ~loud() PRINT_FUNC()
    };
    
    struct aggr
    {
        loud const& l;
        ~aggr() PRINT_FUNC()
    };
    
    int main() {
        auto x = aggr{loud{}};
        std::cout << "end of main\n";
        (void)x;
    }
    

    Live demo

    Note that the destructor of loud is called before the "end of main", whereas x lives until after that trace. Formally, the temporary loud is destroyed at the end of the full-expression which created it.

    The behaviour does not change if the move constructor of aggr is user-defined.

    With mandatory copy-elision in C++17: We identify the object on the rhs S{"bar"} with the object on the lhs b. This causes the lifetime of the temporary to be extended to the lifetime of b. See CWG 1697.


    For the remaining two examples, the move constructor - if called - simply copies the reference. The move constructor (of S) can be elided, of course, but this is not observable since it only copies the reference.

    0 讨论(0)
提交回复
热议问题