【问题标题】:Avoid initializing member until required在需要之前避免初始化成员
【发布时间】:2014-06-24 10:43:00
【问题描述】:

我正在创建一个可以存储 Atom<T> 对象的通用 AtomManager<T> 容器。

enum State { Alive, Dead, Unused };
template<class T> struct Atom
{
    T impl;
    int index, counter;
    State state;
};

我希望 impl 不在堆上,因为我会将 Atom&lt;T&gt; 实例连续存储在管理器中。

AtomManager&lt;T&gt; 将原子存储在 std::vector&lt;Atom&lt;T&gt;&gt; 中,如下所示:

| A | A | A | A | A | A | U | U | U | U |

其中A 表示活着,U 表示未使用。当用户调用AtomManager&lt;T&gt;::refresh()时,所有state等于State::Dead的原子将被移动到存储的末尾,然后它们将被设置为State::Unused。示例:

| A | A | A | A | A | A | U | U | U | U |

// some atoms die

| A | D | A | D | A | A | U | U | U | U |

// user calls refresh()

| A | A | A | A | U | U | U | U | D | D |

// after refresh()

| A | A | A | A | U | U | U | U | U | U |

为了创建原子,我有一个与 T 构造函数签名匹配的函数,这要归功于可变参数模板,并在从存储开头开始的第一个未使用的原子中构造 T

问题是T 必须是default-constructible(因为我在std::vector 上调用resize())。但这是我不需要的,因为我只关心Atom&lt;T&gt;::impl 当状态为State::AliveState::Dead 时。如果原子是活的还是死的,这意味着用户之前使用前面提到的可变参数函数构造了它。

然而,当原子未被使用时,我确实关心Atom&lt;T&gt;::indexAtom&lt;T&gt;::counter(而Atom&lt;T&gt;::impl 是垃圾)。

我不关心原子未使用时Atom&lt;T&gt;::impl的状态。

但是,编译器可以。当T 不是默认可构造时,我不能使用AtomManager&lt;T&gt;

我尝试在Atom&lt;T&gt; 中使用联合:

union { T impl; char dummy; };

...但我无法让它正常工作。

如何在Atom&lt;T&gt; 中存储T 未构造的未初始化实例?

我不关心它的状态。我确信我会在访问它之前正确构建它。但是当原子未使用时,我希望它处于未定义状态。

实现此目的的最佳方法是什么?我不希望 Atom&lt;T&gt;::impl 存储在堆上。

我不想引入额外的依赖项。我不需要查询Atom&lt;T&gt;::impl 的状态,因为我知道什么时候可以安全访问它。

【问题讨论】:

  • @MikeSeymour:我不想在项目中引入boost依赖,也不需要检查Atom&lt;T&gt;::impl是否被初始化。我知道什么时候我可以正确访问它,或者什么时候它应该只是垃圾。
  • BYTE[sizeof(T)] + 新展示位置?
  • 顺便说一句,如果你选择那个技巧,你应该使用 std::aligned_storage 而不是字节数组。
  • std::aligned_storage&lt;T&gt;::type 是一个保证与T 适当对齐的 POD。普通字节数组不保证这一点。

标签: c++ class memory c++11 initialization


【解决方案1】:

你可以使用类似的东西:

template<class T> struct Atom
{
public:
    ~Atom() { if (ptr) ptr->~T(); }

    template <typename...Ts>
    void create(Ts&&... args)
    {
        ptr = new (buffer) T(std::forward<Ts>(args)...);
    }

    T* get() { return ptr; }

private:
    T* ptr = nullptr;
    alignas(T) std::uint8_t buffer[sizeof(T)];
    int index, counter;
    State state;
};

【讨论】:

    【解决方案2】:

    简单的答案是使用boost::optional,但你说你出于某种原因不想这样做。

    要滚动您自己的可选类型,您需要一个字节数组,适合T 对齐。在 C++11 或更高版本中,这很简单:

    alignas(T) char bytes[sizeof(T)];
    

    如果您被一种历史方言所困扰,那么您可能必须使用特定于编译器的扩展来指定对齐方式,或者只是希望获得最好的结果。

    现在您可以使用placement-new 创建对象:

    T * impl = new(bytes) T(...);
    

    最简单的访问方法是通过函数:

    T & get_impl() {return *reinterpret_cast<T*>(bytes);}
    

    别忘了销毁它(但前提是你创建了它):

    get_impl().~T();
    

    【讨论】:

    • 为什么不std::aligned_storage
    • 我不确定这是否违反(严格)别名(3.10):(
    • @dyp:我认为您可以为字符类型设置别名。但我也不太确定。
    • @dyp “为什么不std::aligned_storage?”因为我不熟悉。
    • 在调用析构函数之前应该检查T是否已经被初始化。
    【解决方案3】:

    aligned_storageplacement newexplicit destructor 呼救!

    enum State { Alive, Dead, Unused };
    template<class T> struct Atom
    {
        typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type impl;
        T& data() { return *reinterpret_cast<T*>(&impl); }
        T const& data() const { return *reinterpret_cast<T*>(&impl); }
    
        int index, counter;
        State state;
    
        template<typename... V>
        void create(V&&... args)
        {
            new(static_cast<void*>(&impl))T(std::forward<V>(args)...);
            state = Alive;
        }
    
        void destroy() { data().~T(); state = Unused; }
    
        ~Atom() { if(state != Unused) destroy(); }
    
        Atom(Atom const& other)
            : index(other.index),
            counter(other.counter),
            state(other.state)
        {
            if(state != Unused)
                new(static_cast<void*>(&impl))T(other.data());
        }
    
        Atom(Atom&& other)
            : index(other.index),
            counter(other.counter),
            state(other.state)
        {
            if(state != Unused)
                new(static_cast<void*>(&impl))T(std::move(other.data()));
        }
    };
    

    基本思想是从提供正确对齐的未初始化存储开始。然后,当您希望创建一个对象时,可变参数模板会将您的所有参数转发给一个放置新表达式,该表达式基本上会调用该存储上的构造函数。最后,当你销毁它时(或者Atom的析构函数在它还不是Unused的时候再次被调用),析构函数被调用,这样内存就不再代表一个对象了。

    需要庞大的复制和移动构造函数,以便副本正确映射到Ts 的副本。为简洁起见,省略了编写赋值运算符(例如)。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-19
    • 2018-02-17
    • 2021-08-31
    • 1970-01-01
    • 2021-10-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多