【问题标题】:constexpr and endiannessconstexpr 和字节序
【发布时间】:2010-12-07 17:09:02
【问题描述】:

在 C++ 编程世界中不时出现的一个常见问题是编译时确定字节顺序。通常这是通过几乎不可移植的#ifdefs 来完成的。但是 C++11 constexpr 关键字以及模板特化是否为我们提供了更好的解决方案?

在 C++11 中执行以下操作是否合法:

constexpr bool little_endian()
{
   const static unsigned num = 0xAABBCCDD;
   return reinterpret_cast<const unsigned char*> (&num)[0] == 0xDD;
}

然后为两种字节序类型专门化一个模板:

template <bool LittleEndian>
struct Foo 
{
  // .... specialization for little endian
};

template <>
struct Foo<false>
{
  // .... specialization for big endian
};

然后做:

Foo<little_endian()>::do_something();

【问题讨论】:

    标签: c++ c++11 endianness


    【解决方案1】:

    新答案 (C++20)

    引入了新的标准库头文件&lt;bit&gt;。 除此之外,它还为check the endianness 提供了一种干净、可移植的方式。

    由于我的旧方法依赖于一些有问题的技术,我建议任何使用它的人切换到标准库提供的检查。

    这是一个适配器,它允许使用检查字节顺序的新方法,而无需更新依赖于我的旧类接口的代码:

    #include <bit>
    
    class Endian
    {
    public:
        Endian() = delete;
    
        static constexpr bool little = std::endian::native == std::endian::little;
        static constexpr bool big = std::endian::native == std::endian::big;
        static constexpr bool middle = !little && !big;
    };
    

    旧答案

    我可以这样写:

    #include <cstdint>
    
    class Endian
    {
    private:
        static constexpr uint32_t uint32_ = 0x01020304;
        static constexpr uint8_t magic_ = (const uint8_t&)uint32_;
    public:
        static constexpr bool little = magic_ == 0x04;
        static constexpr bool middle = magic_ == 0x02;
        static constexpr bool big = magic_ == 0x01;
        static_assert(little || middle || big, "Cannot determine endianness!");
    private:
        Endian() = delete;
    };
    

    我已经用 g++ 对其进行了测试,它编译时没有警告。它在 x64 上给出了正确的结果。 如果您有任何大端或中端处理器,请在评论中确认这对您有用。

    【讨论】:

    • 什么是const uint8_t &amp;
    • @Nick 是对常量 8 位无符号整数的引用。
    • 我明白,但这样的演员阵容有什么好处?为什么不只是没有 const 和 ref 的 uint8_t?
    • 我很惊讶这是允许的。看起来标准中的某种遗漏。它不应该被允许,因为它排除了一大类实现。
    • 这不起作用。对const uint8_t&amp; 的强制转换通过将uint32_ 的值截断为8 位来创建一个临时值。这将始终声称每个目标都是小端的。 (尽管这些天在实践中可能已经足够准确了!)
    【解决方案2】:

    假设N2116 是被合并的措辞,那么您的示例格式错误(请注意,C++ 中没有“合法/非法”的概念)。 [decl.constexpr]/3 的拟议文本说

    • 它的函数体应该是一个复合语句的形式 { return expression; } 其中表达式是潜在的常量表达式(5.19);

    您的函数违反了要求,因为它还声明了一个局部变量。

    编辑:可以通过将 num 移出函数来克服此限制。那么,该函数仍然不是格式正确的,因为表达式需要是一个潜在的常量表达式,它被定义为

    一个表达式是一个潜在的常量表达式,如果它是一个常量 替换所有出现的函数参数时的表达式 通过适当类型的任意常量表达式。

    IOW, reinterpret_cast&lt;const unsigned char*&gt; (&amp;num)[0] == 0xDD 必须是一个常量表达式。但是,它不是:&amp;num 将是地址常量表达式 (5.19/4)。但是,常量表达式不允许访问此类指针的值:

    下标运算符 [] 和类成员 access 。和 在创建 地址常量表达式,但不能使用这些运算符访问对象的值。

    编辑:以上文字来自C++98。显然,C++0x 对常量表达式的允许程度更高。该表达式涉及数组引用的左值到右值转换,除非

    它适用于有效整数类型的左值,该左值指 到已初始化的非易失性 const 变量或静态数据成员 用常量表达式

    我不清楚 (&amp;num)[0] 是“指”一个 const 变量,还是只有文字 num“指”这样一个变量。如果(&amp;num)[0] 引用该变量,则不清楚reinterpret_cast&lt;const unsigned char*&gt; (&amp;num)[0] 是否仍然“引用”num

    【讨论】:

    • 我觉得这里不适用。静态变量本身就是常量。
    • N2116 4.1 中的措辞规定函数体必须只有一个语句(即返回语句)。请注意,从我对文本的快速浏览来看,如果 num 是全局定义的,我看不到任何禁止上述代码的内容。
    • 最后引用的段落似乎不是最新的 c++0x 草案 (n2960) 的一部分。草案说&amp;num 是一个常量表达式,如果num 不是线程或自动存储持续时间的变量或数据成员(阅读:如果num 是没有“thread_local”说明符的本地静态或命名空间范围变量, 那么&amp;num 是一个常量表达式)。然而reinterpret_cast 使它成为一个非常量表达式,因为它构成了指针类型到文字类型的转换(注意指针类型本身就是文字类型)。
    • 不,这没有问题。可以肯定的是,这是不允许的。措辞很清楚。
    • 而指针类型是标量类型。 :) 顺便说一句,我认为您也对上述(&amp;num)[0] 感到困惑:在代码中,他从不这样做(&amp;num)[0]。他正在做(reinterpret_cast&lt;...&gt;(&amp;num))[0]。所以你要先考虑reinterpret_cast,然后是result_of_reinterpret_cast[0]。你的最后一段表明你把它的装订弄错了,这让读者很困惑。
    【解决方案3】:

    无法在编译时使用constexpr 确定字节顺序(在 C++20 之前)reinterpret_cast 被 [expr.const]p2 明确禁止,正如 iain 建议从工会的非活跃成员那里读取数据一样。也禁止转换为不同的引用类型,因为这种转换被解释为reinterpret_cast

    更新:

    这现在可以在 C++20 中实现。一种方式(live):

    #include <bit>
    template<std::integral T>
    constexpr bool is_little_endian() {
      for (unsigned bit = 0; bit != sizeof(T) * CHAR_BIT; ++bit) {
        unsigned char data[sizeof(T)] = {};
        // In little-endian, bit i of the raw bytes ...
        data[bit / CHAR_BIT] = 1 << (bit % CHAR_BIT);
        // ... corresponds to bit i of the value.
        if (std::bit_cast<T>(data) != T(1) << bit)
          return false;
      }
      return true;
    }
    static_assert(is_little_endian<int>());
    

    (请注意,C++20 保证二进制补码整数——具有未指定的位顺序——因此我们只需要检查数据的每一位是否映射到整数中的预期位置。)

    但如果你有 C++20 标准库,你也可以问它:

    #include <type_traits>
    constexpr bool is_little_endian = std::endian::native == std::endian::little;
    

    【讨论】:

    【解决方案4】:

    在即将到来的 C++20 中有std::endian

    #include <type_traits>
    
    constexpr bool little_endian() noexcept
    {
        return std::endian::native == std::endian::little;
    }
    

    【讨论】:

    • 我不敢相信他们这么称呼它而不是 endiannessbyte_order 等。
    【解决方案5】:

    这是一个非常有趣的问题。

    我不是语言律师,但您也许可以将 reinterpret_cast 替换为联合。

    const union {
        int int_value;
        char char_value[4];
    } Endian = { 0xAABBCCDD };
    
    constexpr bool little_endian()
    {
       return Endian[0] == 0xDD;
    }
    

    【讨论】:

    • 在联合中放置一个值然后通过另一个成员访问联合是无效的。
    • @GMan:格式正确,但会调用未定义的行为。 “有效”不是 C++ 标准中定义的属性。
    • @Martin:标准的哪 § 中说它调用了未定义的行为? char 左值当然可以给 int 对象起别名(部分)。此外,据我所知,所有可能的位模式都表示有效的 char 和 unsigned char 值。这让我相信这只是调用实现定义的行为,而不是 UB。
    • @sellibitze:使用char* 为指针起别名可以,但不能通过联合。
    • @Martinv.Löwis clang 给出了一个错误,并指出在常量表达式中根本不允许读取联合中的非活动成员。通常它是未定义的行为,但看起来它在常量表达式中格式不正确。
    【解决方案6】:

    我的第一篇文章。只是想分享一些我正在使用的代码。

    //Some handy defines magic, thanks overflow
    #define IS_LITTLE_ENDIAN  ('ABCD'==0x41424344UL) //41 42 43 44 = 'ABCD' hex ASCII code
    #define IS_BIG_ENDIAN     ('ABCD'==0x44434241UL) //44 43 42 41 = 'DCBA' hex ASCII code
    #define IS_UNKNOWN_ENDIAN (IS_LITTLE_ENDIAN == IS_BIG_ENDIAN)
    
    //Next in code...
    struct Quad
    {
        union
        {
    #if IS_LITTLE_ENDIAN
            struct { std::uint8_t b0, b1, b2, b3; };
    
    #elif IS_BIG_ENDIAN
            struct { std::uint8_t b3, b2, b1, b0; };
    
    #elif IS_UNKNOWN_ENDIAN
    #error "Endianness not implemented!"
    #endif
    
            std::uint32_t dword;
        };
    };
    

    constexpr 版本:

    namespace Endian
    {
        namespace Impl //Private
        {
            //41 42 43 44 = 'ABCD' hex ASCII code
            static constexpr std::uint32_t LITTLE_{ 0x41424344u };
    
            //44 43 42 41 = 'DCBA' hex ASCII code
            static constexpr std::uint32_t BIG_{ 0x44434241u };
    
            //Converts chars to uint32 on current platform
            static constexpr std::uint32_t NATIVE_{ 'ABCD' };
        }
    
    
    
        //Public
        enum class Type : size_t { UNKNOWN, LITTLE, BIG };
    
        //Compare
        static constexpr bool IS_LITTLE   = Impl::NATIVE_ == Impl::LITTLE_;
        static constexpr bool IS_BIG      = Impl::NATIVE_ == Impl::BIG_;
        static constexpr bool IS_UNKNOWN  = IS_LITTLE == IS_BIG;
    
        //Endian type on current platform
        static constexpr Type NATIVE_TYPE = IS_LITTLE ? Type::LITTLE : IS_BIG ? Type::BIG : Type::UNKNOWN;
    
    
    
        //Uncomment for test. 
        //static_assert(!IS_LITTLE, "This platform has little endian.");
        //static_assert(!IS_BIG_ENDIAN, "This platform has big endian.");
        //static_assert(!IS_UNKNOWN, "Error: Unsupported endian!");
    }
    

    【讨论】:

    • @A Andrew F. 如果您添加一些代码描述会很棒
    • AFAIK,整数多字符常量的值是实现定义的。至少不是UB。
    【解决方案7】:

    这看起来像是作弊,但您始终可以包含 endian.h... BYTE_ORDER == BIG_ENDIAN 是一个有效的 constexpr...

    【讨论】:

    • 并非所有系统都有 endian.h,MacOS 和 BSD endian.h 也会发出大量警告。
    【解决方案8】:

    如果您的目标是确保编译器在编译时将 little_endian() 优化为常量 true 或 false,而其任何内容不会在可执行文件中结束或在运行时执行,并且仅从“正确”你的两个 Foo 模板之一,我担心你会失望。

    我也不是语言律师,但在我看来,constexpr 就像 inlineregister:一个提醒编译器编写者存在潜在优化的关键字。然后由编译器编写者决定是否利用它。语言规范通常要求行为,而不是优化。

    另外,您是否真的在各种 C++0x 投诉编译器上尝试过这个,看看会发生什么?我猜他们中的大多数人会在你的双模板上窒息,因为如果用false 调用,他们将无法确定使用哪一个。

    【讨论】:

    • 不太一样。 'constexpr' 函数的结果通常可用于需要常量表达式的地方,例如。一个数组边界。虽然我相信在函数模板的情况下还有一些余地。
    猜你喜欢
    • 2016-10-18
    • 2021-03-09
    • 1970-01-01
    • 2014-05-18
    • 1970-01-01
    • 1970-01-01
    • 2020-02-19
    • 2016-08-24
    • 2013-01-01
    相关资源
    最近更新 更多