type-punning:省略放置 new 和析构函数

type-punning: omitting placement new and destructors

提问人:Oersted 提问时间:8/21/2023 最后编辑:Oersted 更新时间:8/22/2023 访问量:101

问:

已经有很多关于严格别名规则和类型双关语的帖子,但我找不到我可以理解的关于对象数组的解释。 我的目标是拥有一个用于存储对象数组的内存池非模板类。 基本上,我只需要在访问时知道实际类型:它可以被看作是一个非模板向量,其迭代器将是模板。 我想到的设计提出了几个问题,所以我将尝试将它们分成几个 SO 问题。

我的问题(这是第二个,见下文)是在算术类型以外的其他情况下是否可以省略放置 new(第 45 行和第 55 行)和相应的析构函数循环 (in)?Deallocate()

#include <cassert>
#include <iostream>
#include <type_traits>

// type that support initialisation from a single double value
using test_t = float;

// just for the sake of the example: p points to at least a sequence of 3 test_t
void load(test_t* p) {
    std::cout << "starting load\n";
    p[0] = static_cast<test_t>(3.14);
    p[1] = static_cast<test_t>(31.4);
    p[2] = static_cast<test_t>(314.);
    std::cout << "ending load\n";
}

// type-punning buffer
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
    // buffer address
    char* p = nullptr;
    // number of stored elements
    size_t n = 0;
    // buffer size in bytes
    size_t s = 0;
    // allocates a char buffer large enough for N object of type T and
    // default-construct them
    // calling it on a previously allocated buffer without adequate call to
    // Deallocate is UB
    template <typename T>
    T* DefaultAllocate(const size_t N) {
        size_t RequiredSize =
            sizeof(std::aligned_storage_t<sizeof(T), alignof(T)>) * N;
        n = N;
        T* tmp;
        if (s < RequiredSize) {
            if (p) {
                delete[] p;
            }
            s = RequiredSize;
            std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
            p = new char[s];
            // placement array default construction
            tmp = new (p) T[N];
            // T* tmp = reinterpret_cast<T*>(p);
            // // optional for arithmetic types and also for trivially
            // destructible
            // // types when we don't care about default values
            // for (size_t i = 0; i < n; ++i) {
            //     new (tmp + i) T();
            // }
        } else {
            // placement array default construction
            tmp = new (p) T[N];
            // T* tmp = reinterpret_cast<T*>(p);
            // // optional for arithmetic types and also for trivially
            // destructible
            // // types when we don't care about default values
            // for (size_t i = 0; i < n; ++i) {
            //     new (tmp + i) T();
            // }
        }
        return tmp;
    }
    // deallocate objects in buffer but not the buffer itself
    template <typename T>
    void Deallocate() {
        T* tmp = reinterpret_cast<T*>(p);
        // Delete elements in reverse order of creation
        // optional for default destructible types
        for (size_t i = 0; i < n; ++i) {
            tmp[n - 1 - i].~T();
        }
        n = 0;
    }
    ~Buffer() {
        if (p) {
            delete[] p;
        }
    }
};

int main() {
    constexpr std::size_t N = 3;
    Buffer B;
    test_t* fb = B.DefaultAllocate<test_t>(N);
    load(fb);
    std::cout << fb[0] << '\n';
    std::cout << fb[1] << '\n';
    std::cout << fb[2] << '\n';
    std::cout << alignof(test_t) << '\t' << sizeof(test_t) << '\n';
    B.Deallocate<test_t>();
    return 0;
}

Live Live
更复杂

为了清楚起见,对于算术类型,完全删除放置 new(仅用 ) 和析构函数循环是否安全? 还有其他类型的类型也可以吗?reinterpret_cast<T*>(p)

注意:我正在使用 C++14,但我也对在最近的标准版本中如何完成它感兴趣。

链接到问题 1
链接到问题 3

[编辑]问题 3 的回答表明,我上面的 C++14 代码片段可能没有正确对齐:这是一个受参考答案启发的更好的版本。
有关一些其他材料,另请参阅问题 1

C++ placement-new type-punning

评论

0赞 Oersted 8/21/2023
@TedLyngmo我在最后做了一个更新,希望它更清楚。关于删除,我错过了删除 nullptr 不是 UB 的事实(我以为是在某个时间点,我很确定我看到了段错误)。那么这个例子确实只适用于默认的可构造类型,但代码片段足够复杂,让它更通用对我的询问毫无意义。
0赞 Ted Lyngmo 8/21/2023
明白了!我以为这是三个问题中的第一个,直到您将链接添加到数字 1 和 3 :-)
1赞 Ted Lyngmo 8/21/2023
与问题无关,但我做了一个小的调整,使分配器在超出范围时销毁对象:一个想法。该类只是我插入的一个测试类,用于查看您的行为。fooBuffer
0赞 Oersted 8/21/2023
@TedLyngmo哇,技术不错!我会花时间完全理解它。您正在将 lambda 用作在模板中声明的擦除类型?std::function
1赞 n. m. could be an AI 8/21/2023
你不应该太在乎。始终使用创建/销毁调用。如果它们可以被优化,它们就会被优化。检查生成的程序集进行验证。

答:

4赞 Ted Lyngmo 8/21/2023 #1

在算术类型以外的其他情况下,是否可以省略放置(第 45 行和第 55 行)和相应的析构函数循环 (in)?newDeallocate()

不。对象的生存期从构造函数开始,到析构函数结束。如果没有这些显式调用,您将只有内存,没有对象。

评论

1赞 François Andrieux 8/22/2023
这个想法是正确的,但措辞缺少一些细节。None-class-types 没有构造函数,也许术语初始化更好。不过,这可以包括默认初始化,令人困惑的是,它不会初始化任何内容,但仍然算作启动 ojbect 的生命周期(如 for )。某些对象(包括某些类类型)可以由某些函数隐式启动其生存期,例如不调用构造函数的地方:en.cppreference.com/w/cpp/language/object#Object_creationint i;memcpy
1赞 François Andrieux 8/22/2023
还有简单析构函数的概念,在解除分配存储之前可以不调用它。调用对象析构函数确实会结束其生存期,但对象的生存期也可以在不涉及其析构函数的情况下结束,例如释放其存储时。
1赞 Ted Lyngmo 8/22/2023
@FrançoisAndrieux 你是对的,因为答案有点宽泛,也有例外。当我有机会时,我可能会将标准中有关隐式生存期类型的一些内容复制到答案中。但是,调用和析构函数将在所有情况下工作,并且很可能在不需要时进行优化,所以这是我想通过的最重要的事情。new[]