3

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

//--------------------------------------------------------------------------------
// 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); }

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


因此,建议的解决方案之一是添加一个接收 的内容的指针new (storage) Derived(std::forward<Ts>(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();
    }
};

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

4

1 回答 1

2

中的std::byte对象storage

reinterpret_cast<Base*>(storage)

将指向数组到指针衰减后,不能与位于该地址的任何对象进行指针互转换。在提供存储Base的数组元素和它为其提供存储的对象之间绝不会出现这种情况。

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

因此,一般来说,对于您尝试从为对象提供存储的数组中获取指向对象的指针的用例,您始终需要std::launderreinterpret_cast.

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

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

应该

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

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

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

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

因此,您需要依靠 ABI 规范来确保Base子对象的地址将等于对象的地址Derived

于 2020-03-29T21:04:45.377 回答