C++ 使用 constexpr 使放置新对齐存储可初始化

C++ make placement new aligned storage initializable using constexpr

这对我来说是一个非常重要的问题,因为它现在是一个瓶颈,我正在尝试研究解决我的问题的可能方法:我需要 constexpr 构造一个 std::function-like class 我用的那个很简单。但是,它使用对齐存储,因此我们可以配置指针大小的捕获元素数。我们称它为函数。

https://github.com/fwsGonzo/libriscv/blob/master/lib/libriscv/util/function.hpp#L91

具体来说,我正在使用最多捕获 1 个指针的函数。通常是“这个”。这些函数运行良好,如果您尝试捕获太多,它们将无法编译。

问题是它们必须在 运行 时间构建,而且它们的数量太多以至于它们使用了大约 3500 纳秒(3.5 微秒),这对我的用例来说是永恒的.我绝对必须找到一种方法来以某种方式降低此设置成本,因此自然的方法是调查我是否可以在编译时构建它们。

我无法这样做,编译器直接告诉我使用 placement new 的构造函数不能在 constexpr 上下文中使用。这个问题讲述了同样的故事:

C++ constexpr in place aligned storage construction

您可以在此处查看有问题的语句: https://github.com/fwsGonzo/libriscv/blob/master/lib/libriscv/util/function.hpp#L148

template<typename Callable>
Function (Callable callable) noexcept
{
    static_assert(sizeof(Callable) <= FunctionStorageSize,
                  "Callable too large (greater than FunctionStorageSize)");
    static_assert(std::is_trivially_copy_constructible_v<Callable>,
                  "Callable not trivially copy constructible");
    static_assert(std::is_trivially_destructible_v<Callable>,
                  "Callable not trivially destructible");

    m_func_ptr = &trampoline<Callable>;

    new(reinterpret_cast<Callable *>(m_storage.data)) Callable(callable);
}

我正在使用 C++20,我愿意接受有关如何解决此问题的建议。鉴于这些函数有一个固定大小的捕获存储和一个函数指针,是否有可能在编译时以某种方式构造它们?不应由此产生堆分配。

使用 C++20,如果您将类型 Callable 的约束增加为 trivially_copyable,您可以使用 bit_cast。您还必须为所有可能的对象大小定义一个包含 aligned_storage <size, alignment> 类型成员的联合。

不幸的是,我认为还没有 bit_cast 的 constexpr 实现。

如果 Callable 指定指向对象类型的指针,则部分解决方案可能是声明一个 constexpr 构造函数:

template<typename Callable>
constexpr
Function (Callable * callable) noexcept
   m_pointer {callable}
   m_func_ptr = &trampoline <Callable>
   {}

//declare the union
union {
    void * m_pointer;
    Storage m_storage;
    };

//end an overload trampoline specialized for pointer to object.

虽然 C++20 确实允许您在 constexpr 上下文中动态分配内存,但在编译时分配的内存不允许泄漏到运行时执行中。因此 constexpr 分配必须静态绑定到常量表达式求值。

即使有 C++20 的特性,你也不能在编译时使用 placement new。

我最终找到了解决这个问题的方法。我的大多数类函数对象只是围绕此函数 class 的原始函数指针,因此我最终尝试使这部分成为 constexpr,并取得了成功。这不是其他人可以回答的问题,因为当您写问题时您无法想到所有内容,而我最终获得了更多信息。尽管如此,对于将来尝试这样做的任何人:您可能无法制作 lambda-with-capture constexpr,但您仍然可以像我所做的那样,如下所示。

通过添加一个与原始函数指针相匹配的新类型,然后像这样在实例化模板中捕获它:

template <>
constexpr Function<RawFunctionPointerType>(RawFunctionPointerType fptr) noexcept
    : m_func_ptr(&trampoline<RawFunctionPointerType>), m_real_ptr{fptr}  {}

m_real_ptr 成员与存储联合:

union {
    RawFunctionPointerType m_real_ptr;
    Storage m_storage;
};

可以 constinit 实例化一个 std::array,它可以在运行时 std::copy 进入我的结构。通过这样做,我最终节省了至少 1 微秒。

我也创建了自己的类型擦除函数。它不是 constexpr,因为我需要使用 placement new 或 std::memcopy 来填充我的存储空间。

主要思想是为“蹦床生成”使用非捕获 lambda,也许您可​​以使用它。 优化后生成的程序集在我眼中看起来非常好... godbolt

#include <iostream>
#include <cstring>

namespace Test
{
    template<typename Return, typename... Args>
    using InvokeFktPtr = Return(*)(const void*, Args...);

    template <
        typename Fkt
    >
    class SingleCastDelegate;

    template <
        typename ReturnType,
        typename... Args
    >
    class SingleCastDelegate<ReturnType(Args...)>
    {
    private:
        InvokeFktPtr<ReturnType, Args...> invokeFktPtr;
    private:
        static constexpr size_t max_lambda_size = 4 * sizeof(void*);
        std::byte storage[max_lambda_size];
    private:
        constexpr const void* GetData() const
        {
            return std::addressof(storage[0]);
        }
        constexpr void* GetData()
        {
            return std::addressof(storage[0]);
        }
    public:
        template<
            typename Lambda
            ,typename PureLambda = std::remove_reference_t<Lambda>
        >
        inline SingleCastDelegate(Lambda&& lambda)
        {
            constexpr auto lambdaSize = sizeof(PureLambda);
            static_assert(lambdaSize <= sizeof(void*) * 4);
            
            //add some static_asserts... (it must be trivial...)
            
            //placement new is not constexpr, or?
            new(std::addressof(storage)) PureLambda(lambda);

            invokeFktPtr = [](const void* data, Args... args)
            {
                const PureLambda& l = *static_cast<const PureLambda*>(data);
                return l(args...);
            };
        }

        template<
            typename... CustomArgs
        >
        using FktPtr = ReturnType(*)(CustomArgs...);

        template<
            typename... CustomArgs
            , typename = typename std::enable_if_t<std::is_invocable_v<FktPtr<Args...>, CustomArgs...>>
        >
        constexpr ReturnType operator()(CustomArgs&&... args) const
        {
            return invokeFktPtr(GetData(), std::forward<CustomArgs>(args)...);
        }
    };
}


int main()
{

    int i = 42;

    auto myFkt = [=](){
        std::cout << i;
    };
    auto myOtherFkt = [=](){
        std::cout << i * 2;
    };
    Test::SingleCastDelegate<void()> fkt = Test::SingleCastDelegate<void()>{ myFkt };
    fkt();

    fkt = myOtherFkt;
    fkt();

    return 0;
}