constexpr 和字节序

Posted

技术标签:

【中文标题】constexpr 和字节序【英文标题】:constexpr and endianness 【发布时间】: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();

【问题讨论】:

【参考方案1】:

新答案 (C++20)

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;

【讨论】:

++ .. 看了this question之后才想到这个。【参考方案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' 函数的结果通常可用于需要常量表达式的地方,例如。一个数组边界。虽然我相信在函数模板的情况下还有一些余地。

以上是关于constexpr 和字节序的主要内容,如果未能解决你的问题,请参考以下文章

主机字节序 和 网络字节序

网络字节序和主机字节序

字节序的问题,为啥GBK和UTF-8没有字节序问题,而UTF-16就有?

主机字节序和网络字节序

字节序

主机字节序和网络字节序转换