首页 文章

c 11严格别名规则是否允许通过char *,char(&)[N]访问uint64_t,甚至std :: array <char,N>&with -fstrict-aliasing -Wstrict-aliasing = 2?

提问于
浏览
4

根据this stackoverflow回答关于 C++11/14 严格的别名规则:

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:对象的动态类型,对象的动态类型的cv限定版本,a类型(如4.4中所定义)类型为对象的动态类型,类型是对应的动态类型的有符号或无符号类型,类型是对应于cv限定版本的有符号或无符号类型对象的动态类型,聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),一种类型,它是对象的动态类型(可能是cv限定的)基类类型,char或unsigned char类型 .

我们可以使用其他类型的存储

(1) char *

(2) char(&)[N]

(3) std::array<char, N> &

不依赖 undefined behavior

constexpr uint64_t lil_endian = 0x65'6e'64'69'61'6e; 
    // a.k.a. Clockwise-Rotated Endian which allocates like
    // char[8] = { n,a,i,d,n,e,\0,\0 }

const auto& arr =   // std::array<char,8> &
    reinterpret_cast<const std::array<char,8> &> (lil_endian);

const auto& carr =  // char(&)[8]>
    reinterpret_cast<const char(&)[8]>           (lil_endian);

const auto* p =     // char *
    reinterpret_cast<const char *>(std::addressof(lil_endian));

int main()
{
    const auto str1  = std::string(arr.crbegin()+2, arr.crend() );

    const auto str2  = std::string(std::crbegin(carr)+2, std::crend(carr) );

    const auto sv3r  = std::string_view(p, 8);
    const auto str3  = std::string(sv3r.crbegin()+2, sv3r.crend() );

    auto lam = [](const auto& str) {
        std::cout << str << '\n'
                  << str.size() << '\n' << '\n' << std::hex;
        for (const auto ch : str) {
            std::cout << ch << " : " << static_cast<uint32_t>(ch) << '\n';
        }
        std::cout << '\n' << '\n' << std::dec;
    };

    lam(str1);
    lam(str2);
    lam(str3);
}

所有lambda调用产生:

endian
6

e : 65
n : 6e
d : 64
i : 69
a : 61
n : 6e

godbolt.org/g/cdDTAM(启用-fstrict-aliasing -Wstrict-aliasing = 2)

wandbox.org/permlink/pGvPCzNJURGfEki7

2 回答

  • 3

    char(&)[N] case和 std::array<char, N> case都会导致未定义的行为 . 原因已经被你引用了 . 请注意, char(&)[N]std::array<char, N>char 的类型不同 .

    我不确定 char 情况,因为当前标准没有明确说明一个对象可以被视为一个窄字符数组(有关进一步的讨论,请参阅here) .

    无论如何,如果要访问对象的基础字节,请使用 std::memcpy ,正如标准在[basic.types]/2中明确指出的那样:

    对于普通可复制类型T的任何对象(基类子对象除外),无论对象是否包含类型T的有效值,组成对象的基础字节([intro.memory])都可以复制到char,unsigned char或std :: byte([cstddef.syn])的数组 . 如果将该数组的内容复制回对象,则该对象应随后保持其原始值 . [例子:#define N sizeof(T)
    char buf [N];
    T obj; // obj初始化为其原始值
    std :: memcpy(buf,&obj,N); //在这两次调用std :: memcpy之间,可能会修改obj
    std :: memcpy(&obj,buf,N); //此时,标量类型的obj的每个子对象都保持其原始值

    • 结束例子]
  • 2

    严格的别名规则实际上非常简单:如果一个不是另一个的子对象,那么具有重叠生命周期的两个对象不能具有重叠的存储区域 . (*)

    然而,允许读取对象的内存表示 . 对象的内存表示是 unsigned char [basic.types] / 4的序列:

    类型T的对象的对象表示是由类型T的对象占据的N个无符号字符对象的序列,其中N等于sizeof(T) . 对象的值表示是保存类型T的值的位集 .

    因此在你的例子中:

    • lam(str1) 是UB(未定义行为);

    • lam(str2) 是UB(一个数组,它的第一个元素不是pointer interconvertible);

    • lam(str3) 未在标准中声明为UB,如果您将 char 替换为 unsigned char ,则可能会认为您正在读取对象表示 . (它也没有定义,但它应该适用于所有编译器)

    因此,使用第三种情况并将 p 的声明更改为 const unsigned char* 应始终产生预期结果 . 对于其他两种情况,它可以使用这个简单的示例,但如果代码更复杂或者在更新的编译器版本上可能会中断 .


    (*)此规则有两个例外:一个用于具有共同初始化序列的工会成员;和一个 unsigned charstd::byte 数组,为其他对象提供存储 .

相关问题