带有就地多态容器的 std::launder

Posted

技术标签:

【中文标题】带有就地多态容器的 std::launder【英文标题】:std::launder with inplace polymorphic containers 【发布时间】:2020-07-10 04:53:05 【问题描述】:

我正在用 C++ 为 Game Boy Advance 做一个有点不平凡的项目,而且,作为一个完全没有内存管理的有限平台,我试图避免调用 malloc 和动态分配。为此,我实现了相当数量的,所谓的“就地多态容器”,它存储从Base 类派生的类型的对象(在类型模板中参数化),然后我有@ 987654326@对象并使用完美转发调用相应的构造函数。例如,其中一个容器如下所示(也可访问here):

//--------------------------------------------------------------------------------
// PointerInterfaceContainer.hpp
//--------------------------------------------------------------------------------
// Provides a class that can effectively allocate objects derived from a
// base class and expose them as pointers from that base
//--------------------------------------------------------------------------------
#pragma once

#include <cstdint>
#include <cstddef>
#include <algorithm>
#include "type_traits.hpp"

template <typename Base, std::size_t Size>
class alignas(max_align_t) PointerInterfaceContainer

    static_assert(std::is_default_constructible_v<Base>,
        "PointerInterfaceContainer will not work without a Base that is default constructible!");
    static_assert(std::has_virtual_destructor_v<Base>,
        "PointerInterfaceContainer will not work properly without virtual destructors!");
    static_assert(sizeof(Base) >= sizeof(std::intptr_t),
        "PointerInterfaceContainer must not be smaller than a pointer");

    std::byte storage[Size];

public:
    PointerInterfaceContainer()  new (storage) Base(); 

    template <typename Derived, typename... Ts>
    void assign(Ts&&... ts)
    
        static_assert(std::is_base_of_v<Base, Derived>,
            "The Derived class must be derived from Base!");
        static_assert(sizeof(Derived) <= Size,
            "The Derived class is too big to fit in that PointerInterfaceContainer");
        static_assert(!is_virtual_base_of_v<Base, Derived>,
            "PointerInterfaceContainer does not work properly with virtual base classes!");

        reinterpret_cast<Base*>(storage)->~Base();
        new (storage) Derived(std::forward<Ts>(ts)...);
    

    void clear()  assign<Base>(); 

    PointerInterfaceContainer(const PointerInterfaceContainer&) = delete;
    PointerInterfaceContainer(PointerInterfaceContainer&&) = delete;
    PointerInterfaceContainer &operator=(PointerInterfaceContainer) = delete;

    Base* operator->()  return reinterpret_cast<Base*>(storage); 
    const Base* operator->() const  return reinterpret_cast<const Base*>(storage); 

    Base& operator*()  return *reinterpret_cast<Base*>(storage); 
    const Base& operator*() const  return *reinterpret_cast<const Base*>(storage); 

    ~PointerInterfaceContainer()
    
        reinterpret_cast<Base*>(storage)->~Base();
    
;

看了一些关于std::launder的文章后,我还是有疑问,但我猜这几行代码可能会导致问题:

Base* operator->()  return reinterpret_cast<Base*>(storage); 
const Base* operator->() const  return reinterpret_cast<const Base*>(storage); 

Base& operator*()  return *reinterpret_cast<Base*>(storage); 
const Base& operator*() const  return *reinterpret_cast<const Base*>(storage); 

特别是如果有问题的Deriveds(或Base 本身)有const 成员或引用。我要问的是一个一般指南,不仅针对这个(和另一个)容器,还有关于std::launder 的使用。你觉得这里怎么样?


因此,建议的解决方案之一是添加一个指针,该指针将接收new (storage) Derived(std::forward&lt;Ts&gt;(ts)...); 的内容,如下所示:

//--------------------------------------------------------------------------------
// PointerInterfaceContainer.hpp
//--------------------------------------------------------------------------------
// Provides a class that can effectively allocate objects derived from a
// base class and expose them as pointers from that base
//--------------------------------------------------------------------------------
#pragma once

#include <cstdint>
#include <cstddef>
#include <algorithm>
#include <utility>
#include "type_traits.hpp"

template <typename Base, std::size_t Size>
class alignas(max_align_t) PointerInterfaceContainer

    static_assert(std::is_default_constructible_v<Base>,
        "PointerInterfaceContainer will not work without a Base that is default constructible!");
    static_assert(std::has_virtual_destructor_v<Base>,
        "PointerInterfaceContainer will not work properly without virtual destructors!");
    static_assert(sizeof(Base) >= sizeof(std::intptr_t),
        "PointerInterfaceContainer must not be smaller than a pointer");

    // This pointer will, in 100% of the cases, point to storage
    // because the codebase won't have any Derived from which Base
    // isn't the primary base class, but it needs to be there because
    // casting storage to Base* is undefined behavior
    Base *curObject;
    std::byte storage[Size];

public:
    PointerInterfaceContainer()  curObject = new (storage) Base(); 

    template <typename Derived, typename... Ts>
    void assign(Ts&&... ts)
    
        static_assert(std::is_base_of_v<Base, Derived>,
            "The Derived class must be derived from Base!");
        static_assert(sizeof(Derived) <= Size,
            "The Derived class is too big to fit in that PointerInterfaceContainer");
        static_assert(!is_virtual_base_of_v<Base, Derived>,
            "PointerInterfaceContainer does not work properly with virtual base classes!");

        curObject->~Base();
        curObject = new (storage) Derived(std::forward<Ts>(ts)...);
    

    void clear()  assign<Base>(); 

    PointerInterfaceContainer(const PointerInterfaceContainer&) = delete;
    PointerInterfaceContainer(PointerInterfaceContainer&&) = delete;
    PointerInterfaceContainer &operator=(PointerInterfaceContainer) = delete;

    Base* operator->()  return curObject; 
    const Base* operator->() const  return curObject; 

    Base& operator*()  return *curObject; 
    const Base& operator*() const  return *curObject; 

    ~PointerInterfaceContainer()
    
        curObject->~Base();
    
;

但这基本上意味着代码中每个PointerInterfaceContainer 的开销为sizeof(void*) 字节(在相关架构中为4)。这似乎不是很多,但是如果我想塞满 1024 个容器,每个容器有 128 个字节,那么这个开销就会加起来。另外,它需要第二次内存访问才能访问指针,并且在 99% 的情况下,Derived 将有 Base 作为主要基类(这意味着 static_cast&lt;Derved*&gt;(curObject)curObject 是相同的位置),这意味着指针将始终指向storage,这意味着所有这些开销都是完全没有必要的。

【问题讨论】:

@rsjaffe 您的链接是this answer 的镜像,我已经阅读过了。我几乎可以肯定我需要在这里使用std::launder,但我想与专业人士再次确认。谢谢! 是的,我发现它是答案的镜像,这就是我删除它的原因。很抱歉打扰您。 一篇有趣的论文,如果你还没有看过的话:open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0532r0.pdf. 据我所知,您需要在代码中的所有 reinterpret_cast 场合使用 launder,因为您投射了非指针可相互转换的指针。 【参考方案1】:

storage 所在的 std::byte 对象

reinterpret_cast<Base*>(storage)

将指向数组到指针衰减后,与位于该地址的任何Base 对象都不是指针互转换。数组的元素提供存储和它提供存储的对象之间永远不会发生这种情况。

Pointer-interconvertibility 基本上仅适用于您在标准布局类及其成员/基类之间转换指针的情况(并且仅在特殊情况下)。这些是唯一不需要std::launder 的情况。

因此,一般来说,对于您尝试从为对象提供存储的数组中获取指向对象的指针的用例,您总是需要在@987654329 之后应用std::launder @。

因此,在您目前使用reinterpret_cast 的所有情况下,您必须始终使用std::launder。例如:

reinterpret_cast<Base*>(storage)->~Base();

应该是

std::launder(reinterpret_cast<Base*>(storage))->~Base();

但是请注意,从 C++ 标准的角度来看,您尝试做的事情仍然不能保证有效,并且没有标准的方式来强制它工作。

你的类Base 需要有一个虚拟析构函数。这意味着Base 以及从它派生的所有类都不是standard-layout。非标准布局的类实际上无法保证其布局。这意味着您无法保证Derived 对象的地址等于Base 子对象的地址,无论您如何让DerivedBase 继承。

如果地址不匹配,std::launder 将具有未定义的行为,因为在您执行 new(storage) Derived 之后,该地址将不会有 Base 对象。

因此您需要依靠 ABI 规范来确保 Base 子对象的地址与 Derived 对象的地址相同。

【讨论】:

所以基本上我避免malloc 的努力是徒劳的? @JoaoBapt 如果你知道 ABI 规范,你可以依赖它。它不会是便携的,但对于如此专业的东西来说这应该不是问题。 这是 ARM Embedded ABI arm-none-eabi,所以我可以依赖它。我在做一个(粗体)假设,即“首先”从Base 派生的Derived 类将具有相同的地址。另一种可能性是包含Base* curObj; 并将其设置为new (storage) Derived(std::forward&lt;Ts&gt;(ts)...) 返回的值,但这意味着我会为每个使用的对象浪费sizeof(void*) 字节。 @JoaoBapt 是的,我正要推荐第二种选择。我不知道 ABI 是怎么说的,但我想如果 Base 是第一个指定的基类,布局很有可能是需要的。 或者然后扔掉所有对多态对象的大惊小怪,并在上面使用面向数据的设计/ECS ?

以上是关于带有就地多态容器的 std::launder的主要内容,如果未能解决你的问题,请参考以下文章

为啥这里需要 std::launder ?

为啥要引入 `std::launder` 而不是让编译器处理它?

我在哪里可以找到 std::launder 的真正作用? [复制]

如何解释 std::launder 的可达性要求?

std::launder 替代 pre c++17

std::launder 的效果是不是在调用它的表达式之后持续?