【问题标题】:Strict aliasing, and alignment questions when partitioning memory分区内存时的严格混叠和对齐问题
【发布时间】:2018-01-18 21:43:42
【问题描述】:

我的目标是分配一块内存,然后将其划分为不同类型的较小数组。我对我在这里写的代码有几个问题:

#include <iostream>
#include <cstdint>
#include <cstdlib>

int main() {
    constexpr std::size_t array_count   = 5;
    constexpr std::size_t elements_size = sizeof(std::uint32_t) + sizeof(std::uint16_t);

    void* const pv = std::calloc(array_count, elements_size);

    //Partition the memory. p32 array starts at pv, p16 array starts after the 20 byte buffer for the p32 array.
    std::uint32_t* const p32 = (std::uint32_t *) pv;
    std::uint16_t* const p16 = (std::uint16_t *)((char *) pv + sizeof(std::uint32_t) * array_count);

    //Initialize values.
    for(std::size_t i = 0; i < array_count; ++i) {
        p32[i] = i;
        p16[i] = i * 2;
    }

    //Read them back.
    for(std::size_t i = 0; i < array_count; ++i) {
        std::cout << p32[i] << std::endl;
        std::cout << p16[i] << std::endl;
        std::cout << std::endl;
    }

    std::free(pv);
}
  1. 此代码是否违反 c++ 的严格别名规则?从 malloc 或 calloc 调用中转换指针时,我无法找到有关别名的资源。 p32 和 p16 指针不应重叠。
  2. 如果我反转两个数组的定位,其中 p16 从 pv 开始,并且 p32 与 pv 有 10 字节偏移,这可能会导致段错误,因为 uint32_t 与 4 字节边界对齐 pv + 10 可能在 2 字节上边界,对吧?
  3. 此程序是否不安全,或引入了我通常缺少的任何未定义行为?我在本地机器上得到了预期的输出,但这并不意味着我的代码是正确的。

【问题讨论】:

    标签: c++ pointers memory-management c++14


    【解决方案1】:

    是的,程序是 UB。当你这样做时:

    for(std::size_t i = 0; i < array_count; ++i) {
        p32[i] = i;
        p16[i] = i * 2;
    }
    

    没有p32 或p16 指向的uint32_t 或uint16_t 对象。 calloc 只给你字节,而不是对象。你不能只存在reinterpret_cast 对象。最重要的是,索引只针对数组定义,p32 不指向数组。

    要使其明确定义,您必须创建一个数组对象。但是,数组的placement-new 是broken,因此您需要手动初始化一堆uint32_ts,例如:

    auto p32 = reinterpret_cast<uint32_t*>(pv);
    for (int i = 0; i < array_count; ++i) {
        new (p32+i) uint32_t; // NB: this does no initialization, but it does satisfy
                              // [intro.object] in actually creating an object
    }
    

    这将遇到一个单独的问题:CWG 2182。现在我们有array_countuint32_ts,但我们没有uint32_t[array_count],所以索引仍然是UB。基本上,完全按标准的 C++ 编写是没有办法的。另见my similar question on the topic。


    也就是说,在野外执行此操作的代码量是巨大的,并且每个实现都允许您这样做。

    【讨论】:

    • 从calloc 获得n 字节后,您可以重新解释为您想要的任何内容。我认为这是对动态内存的正常处理。虽然您没有触及分配内存的边界,但您无法获得 UB。
    • @FilipKočica “正常处理”并不意味着它不是 UB。还是UB。没有对象。
    • 是的。引用C++14 basic.life:“类型 T 的对象的生命周期开始于: — 获得类型 T 的正确对齐和大小的存储,并且 — 如果对象具有非平凡初始化,则其初始化完成。 ".因此对我来说,OP 的代码似乎定义明确。
    • @Deduplicator 是的,处理了这个问题,然后添加了另一个可笑的案例。
    • 一路下来都是乌龟。啊!
    【解决方案2】:

    我只解决严格别名问题的一部分。

    C++ 标准很少谈论 malloc - 主要提到它具有在 C 中定义的语义。在严格阅读 C++ 标准时,没有违反别名规则,因为没有别名的 object - 在 C++ 中,对象的生命周期从构造后开始,malloc 调用未构造任何对象。

    因此,这是标准未指定的内容(与未定义相反)。

    【讨论】:

      【解决方案3】:
      1. 不,这只是malloc 返回值和处理分配的内存的正常转换。您可以将这些字节重新解释为您想要的任何数据类型。直到您触摸已分配块边界后面的内存。

      2. 否

      3. 我知道您只是在询问和尝试,但您通常应该分配内存。

        std::uint32_t* const p32 = std::calloc(array_count, sizeof(std::uint32_t));

        std::uint16_t* const p16 = std::calloc(array_count, sizeof(std::uint16_t));

      【讨论】:

      • 我没有按照您对第 2 点的回答。他为什么要触摸分配块后面的 2B?并且将指针的每次使用都称为不安全是一种矫枉过正。此外,合并分配可能是编写高效代码的一个重要方面,因为分配很昂贵。
      • 不是,他说的是交换两个子块的顺序。
      【解决方案4】:

      这个程序不安全吗?

      在您的示例中,我认为异常完全没有问题,但我想您打算在另一个上下文中使用它。如果您的代码抛出异常,您可能会泄漏一些内存。

      你真的需要使用 calloc/free 吗?在 cppcore 指南中,您可以找到一些关于 C++ 资源管理的指南: https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#S-resource

      例如,R10 和 R11 说“不要显式调用 malloc/calloc/free,new/delete”:https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rr-mallocfree

      只有当你在做一些真正需要非常底层代码的事情时,你才需要显式调用 new 和 delete,但是 C++ 的方式是将所有这些调用封装在一个类中,所以类的用户不需要显式调用 new 和 delete。

      但是如果你不做一些低级的事情,你有没有考虑使用 std::array?还是 std::vector?

      【讨论】:

        猜你喜欢
        • 2012-12-19
        • 1970-01-01
        • 2012-11-08
        • 2023-03-15
        • 1970-01-01
        • 2013-03-27
        • 1970-01-01
        • 2011-05-25
        • 1970-01-01
        相关资源
        最近更新 更多