Search code examples
c++bitset

Lifetime of std::bitset::reference object created in std::bitset::operator[]?


I have been looking at header file of bitset standard C++ library header. I found out that overloaded operator[] operator[](size_t ndx) (defined in class bitset) returns a temproray object of class reference.

reference
    operator[](size_t __position)
{ return reference(*this,__position); }

This overloaded operator encapsulates the concept of a single bit. An instance of this class is a proxy for an actual bit. It can be useful in expressions like

bitset<10> b;
b[2] = true;

The reference class has defined overloaded = operator member function so that above example can work:

 //For b[i] = __x;
 reference&
     operator=(bool __x)
 {
   if (__x)
     *_M_wp |= _Base::_S_maskbit(_M_bpos);
   else
     *_M_wp &= ~_Base::_S_maskbit(_M_bpos);
   return *this;
}

However, i am confused over this expression:

if (b[2]) {
    //Do something
}

The b[2] first returns a temporary object of class reference and then overloaded operator (operator bool() const) is called on that returned temporary object to convert it to bool data type.

// For __x = b[i];
operator bool() const
{ return (*(_M_wp) & _Base::_S_maskbit(_M_bpos)) != 0; }

If temporary objects (object having automatic storage class) are created on stacks, then calling an another function (operator bool() const) shouldn't destroys the temporary object returned by the first function call (reference object returned by reference operator[](size_t __position))?

What is the lifetime of temporary objects in C and C++?


Solution

  • From class.temporary#4, emphasized is mine.

    When an implementation introduces a temporary object of a class that has a non-trivial constructor ([class.ctor], [class.copy]), it shall ensure that a constructor is called for the temporary object. Similarly, the destructor shall be called for a temporary with a non-trivial destructor ([class.dtor]). Temporary objects are destroyed as the last step in evaluating the full-expression ([intro.execution]) that (lexically) contains the point where they were created. This is true even if that evaluation ends in throwing an exception. The value computations and side effects of destroying a temporary object are associated only with the full-expression, not with any specific subexpression.

    That temporary object will be destroyed at the last step of that given expression.

    In fact, C++ relies on it for this very common expression can be work correctly:

    int x = 0, y = 0, z = 0, t = 0;
    int a = x + y + z + t;
    

    Because x+y is a temporary, x+y+z is another temporary.


    The lifetime of temporary will be shortened following rules in class.temporary#5

    There are three contexts in which temporaries are destroyed at a different point than the end of the full-expression. The first context is when a default constructor is called to initialize an element of an array with no corresponding initializer ([dcl.init]). The second context is when a copy constructor is called to copy an element of an array while the entire array is copied ([expr.prim.lambda], [class.copy]). In either case, if the constructor has one or more default arguments, the destruction of every temporary created in a default argument is sequenced before the construction of the next array element, if any.

    and it will be prolonged following rule in class.temporary#6:

    The third context is when a reference is bound to a temporary.116 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:

    • A temporary object bound to a reference parameter in a function call ([expr.call]) persists until the completion of the full-expression containing the call.

    • The lifetime of a temporary bound to the returned value in a function return statement ([stmt.return]) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.

    • A temporary bound to a reference in a new-initializer ([expr.new]) persists until the completion of the full-expression containing the new-initializer.

    The first context can be seen in this example:

    struct bar {
        bar() { std::cout << __func__ << '\n'; }
        bar(const bar&) { std::cout << __func__ << "__\n"; }
        ~bar() { std::cout << __func__ << '\n'; }
    };
    
    struct foo {
        foo(const bar& b = bar()) { std::cout << __func__ << '\n'; }
    };
    
    int main() {
        foo f[] = {foo(), foo()};
    }
    

    Above program should output:

    bar
    foo
    ~bar
    bar
    foo
    ~bar
    

    The second context will be added to C++17, from then this program:

    struct bar {
        bar() { std::cout << __func__ << '\n'; }
        bar(const bar&) { std::cout << __func__ << "__\n"; }
        ~bar() { std::cout << __func__ << '\n'; }
    };
    
    struct foo {
        foo() {}
        foo(const foo&, const bar& b = bar()) { std::cout << __func__ << "__\n"; }
    };
    
    struct foox {
        foo f[2];
    };
    
    int main() {
        foox fx;
        foox yx = fx;
    }
    

    must output:

    bar
    foo__
    ~bar
    bar
    foo__
    ~bar
    

    For the third context, you can find the answer in here, and here