Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Placement New and Perfect Forwarding

Tags:

c++

c++11

I have the following code that intends to create an array, but without default initialization of its objects. I would like to forward perfectly to placement new, which seems to happen, but I find that the objects' destructor is called inside the emplace function.

#include <iostream>
#include <memory> // std::uninitialized_copy, std::allocator...
#include <utility> // std::move...
#include <bitset>


struct Int {

    int i;

    Int ( ) : i ( -1 ) { std::cout << "default constructed\n"; }
    Int ( const int i_ ) : i ( i_ ) { std::cout << i << " constructed\n"; }
    Int ( Int && int_ ) : i ( std::move ( int_.i ) ) { std::cout << i << " move constructed\n"; }
    Int ( const Int & int_ ) : i ( int_.i ) { std::cout << i << " copy constructed\n"; }
    ~Int ( ) { std::cout << i << " destructed\n"; i = -1; }
};


template <typename T, size_t S = 64>
class NoInitArray {

    std::bitset<S> m_used;

    T *m_array = reinterpret_cast < T* > ( ::operator new ( sizeof ( T ) * S ) );

public:

    T const &operator [ ] ( const size_t idx_ ) const {

        return m_array [ idx_ ];
    }

    NoInitArray ( ) { }

    ~NoInitArray ( ) {

        for ( size_t idx = 0; idx < S; ++idx ) {

            if ( m_used [ idx ] ) {

                reinterpret_cast< const T* > ( m_array + idx )->~T ( );
            }
        }
    }

    template<typename ...Args>
    void emplace ( const size_t idx_, Args &&... value_ ) {

        std::cout << "start emplace\n";

        m_used [ idx_ ] = 1;

        new ( m_array + idx_ ) T ( std::forward<T> ( value_ ) ... );

        std::cout << "end emplace\n";
    }
};


int main ( ) {

    NoInitArray<Int> nia;

    nia.emplace ( 0, 0 );
    nia.emplace ( 1, 1 );

    std::cout << nia [ 1 ].i << std::endl;

    nia.emplace ( 2, 2 );

    return 0;
}

The result of running this program is as follows:

start emplace
0 constructed
0 move constructed
0 destructed
end emplace
start emplace
1 constructed
1 move constructed
1 destructed
end emplace
1
start emplace
2 constructed
2 move constructed
2 destructed
end emplace
0 destructed
1 destructed
2 destructed

It shows that the objects are constructed once and destructed twice (which obviously is UB), once inside the emplace function, and then once at destruction of the NoInitArray.

The question is "Why is the destructor of my Int object called inside the emplace function"?

Compiler, latest Clang/LLVM on Windhoze.

EDIT1: I've added move and copy constructors to the Int struct, now the count matches, i.e. 2 constructions and 2 destructions.

EDIT2: Changing the Placement New line from new ( m_array + idx_ ) T ( std::forward<T> ( value_ ) ... ); to new ( m_array + idx_ ) T ( value_ ... ); avoids the superfluous construction/destruction, without the need for a move constructor.

EDIT3: Just for future readers. As per above, the ~NoInitArray() leaks memory. Calling delete on m_array is bad news as well as this calls (in Clang/LLVM) the destructor of m_array [ 0 ] (but as far as I've understood now, that is in no way guaranteed, i.e. UB). std::malloc/std::free seems to be the way to go, but some say that if you do that all hell will break lose, and one may lose a leg.

like image 550
degski Avatar asked Feb 06 '23 19:02

degski


2 Answers

"It shows that the objects are constructed once and destructed twice" is not true. The output X move constructed should be included as one construction so the constructions are twice.

The line

new ( m_array + idx_ ) T ( std::forward<T> ( value_ ) ... );

should be

new ( m_array + idx_ ) T ( std::forward<Args&&> ( value_ )... );

std::forward<T>(value_) calls the constructor when T=Int, and this temporary object is moved, so there is an extra move constructor call.

EDIT

In your edit 2 you replace the line without std::forward anymore. In this case, OK, but the differences emerge when you call the emplace like this

    nia.emplace ( 0, Int(0) );

Without std::forward, new T(value_...) would call the copy constructor, while new T(std::forward<Args&&>(value_)...) would call the move constructor.

EDIT-2

It should be new T(std::forward<Args>(value_)...). Thanks to @Constantin Baranov.

like image 83
neuront Avatar answered Feb 10 '23 15:02

neuront


I think the constructor and destructor are called in the step: std::forward<T> ( value_ ) in new ( m_array + idx_ ) T ( std::forward<T> ( value_ ) ... ).

The std::forward<T>(value_) will create a temp value T.

like image 22
corey Avatar answered Feb 10 '23 13:02

corey