Placement New和完美转发

4

我有以下代码,意图创建一个数组,但不对其对象进行默认初始化。我希望完美地转发到放置 new,这似乎已经发生,但我发现对象的析构函数在 emplace 函数内被调用。

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

运行该程序的结果如下所示:
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

它显示对象被构造一次并被析构两次(这显然是未定义行为),一次在emplace函数内部,然后在NoInitArray的销毁时再次销毁。
问题是:“为什么我的Int对象的析构函数在emplace函数内部调用?”
编译器使用最新的Clang/LLVM,运行在Windhoze上。
EDIT1:我将移动和拷贝构造函数添加到Int结构体中,现在数量匹配,即2个构造和2个析构。
EDIT2: 将放置new行从new (m_array + idx_) T(std::forward<T>(value_)...);更改为new (m_array + idx_) T(value_...);可以避免多余的构造/析构,不需要移动构造函数。
EDIT3:仅供未来读者参考。如上所述,~NoInitArray()泄漏内存。调用delete m_array也是坏消息,因为它调用(在Clang/LLVM中)m_array [0]的析构函数(但据我现在所理解的,这无论如何都不能保证,即未定义行为)。std::malloc / std::free似乎是正确的方法,但有人说,如果您这样做,所有地狱都会敞开大门,并且可能失去一条腿。

1
一定要记得统计复制构造函数的调用次数。 - Ben Voigt
@BenVoigt,复制构造函数不是调用普通构造函数吗?如果这是问题所在,也就是说转发没有按预期工作,那么我该如何使其正常工作,即移动对象而不是复制它? - degski
@BenVoigt,多亏了您的建议,我已经解决了问题。在放置new中去掉std :: forward就消除了复制。感谢您激发我的思维。 - degski
@ChristopherOicles 当然我知道那个,但还是谢谢你的第二个建议。 - degski
如果你的真实代码中不存在该语句,请确保 emplace 检查 m_used[ idx_ ] 是否存在构造对象,如果是,则在进行放置 new 之前调用 m_array[idx_].~T()。 - Christopher Oicles
显示剩余3条评论
2个回答

4

我认为构造函数和析构函数在以下步骤中被调用:std::forward<T> ( value_ )new ( m_array + idx_ ) T ( std::forward<T> ( value_ ) ... ) 中。

std::forward<T>(value_) 将创建一个临时的值 T。


我只能接受一个答案,neuront的答案稍微更完整,所以我接受了他的答案,谢谢你的回复。 - degski

4
"它表明对象只被构造了一次,但被析构了两次"并不正确。输出的"X move constructed"应该算作一个构造,因此构造函数执行了两次。

这行代码:

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

应该是

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

std::forward<T>(value_)函数在T=Int时调用构造函数,并移动这个临时对象,因此会多一个移动构造函数的调用。

编辑

在您的第二次编辑中,您替换了没有std::forward的那行代码。在这种情况下,可以,但当您像这样调用emplace时,差异就出现了。

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

如果没有使用std::forward, new T(value_...) 将会调用复制构造函数,而new T(std::forward<Args&&>(value_)...)将会调用移动构造函数。

EDIT-2

正确的写法应该是 new T(std::forward<Args>(value_)...)。感谢 @Constantin Baranov 的指正。


感谢您的回复,正如我在EDIT2中所指出的,在这种情况下,它也可以按照我所描述的方式工作。我的做法是否正确?或者在一个非平凡的情况(不像这个)中,使用您的解决方案是必须的吗?结果似乎是相同的,据我所知没有调用移动构造函数。 - degski
2
应该是 std::forward<Args>(value_)。幸运的是,据我所见,额外的 && 并不会使事情变得更糟。 - Constantin Baranov
@ConstantinBaranov 是的,你说得对。forward 的返回类型包含一个 &&,所以这是不必要的。谢谢你指出这一点。 - neuront
1
@degski 这只是一个例子,来说明它们之间的区别。 - neuront
你的第一次编辑有些误导,因为你谈论的是value...对象的复制和移动构造函数,而不是任何T对象。 - Ben Voigt
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接