符合标准的主机到网络字节顺序转换

Standard compliant host to network endianess conversion

我很惊讶 Whosebug 上有多少主题涉及找出系统的字节序和转换字节序。更令我惊讶的是,这两个问题竟然有数百种不同的答案。到目前为止,我所看到的所有提议的解决方案都是基于未定义的行为、非标准编译器扩展或 OS 特定的头文件。在我看来,如果现有答案给出了符合标准、高效(例如,使用 x86-bswap)、编译时间启用的解决方案,则此问题只是一个重复问题。

肯定有一个符合标准的解决方案可用,我无法在一大堆旧的“hacky”解决方案中找到它。标准库不包含这样的功能也有些奇怪。也许对此类问题的态度正在改变,因为 C++20 在标准中引入了一种检测字节序的方法(通过 std::endian),而 C++23 可能会包含 std::byteswap,这会翻转字节序。

无论如何,我的问题是:

  1. 从哪个 C++ 标准开始,是否有可移植的符合标准的方式来执行主机到网络的字节顺序转换?

  2. 我在下面论证它在 C++20 中是可能的。我的代码是否正确,是否可以改进?

  3. 这种纯 C++ 解决方案是否应该优于 OS 特定函数,例如 POSIX-htonl ? (我想是的)

我想我可以给出一个 C++23 解决方案,它是 OS-独立的、高效的(没有系统调用,使用 x86-bswap ) 并可移植到小端和大端系统(但不能移植到混合端系统):

// requires C++23. see https://gcc.godbolt.org/z/6or1sEvKn
#include <type_traits>
#include <utility>
#include <bit>

constexpr inline auto host_to_net(std::integral auto i) {
    static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
    if constexpr (std::endian::native == std::endian::big) {
        return i;
    } else {
        return std::byteswap(i);
    }
}

由于 std::endian 在 C++20 中可用,因此可以通过实现 byteswap 手动。 中描述了一个解决方案,引用:

// requires C++17
#include <climits>
#include <cstdint>
#include <type_traits>

template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) {
  return ((((i >> (N * CHAR_BIT)) & (T)(unsigned char)(-1)) <<
           ((sizeof(T) - 1 - N) * CHAR_BIT)) | ...);
}; //                                        ^~~~~ fold expression
template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) {
  return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}

链接的答案还提供了一个 C++11 byteswap,但那个似乎效率较低 (not compiled to x86-bswap)。我认为也应该有一种有效的 C++11 方法来做到这一点(使用更少的模板废话甚至更多),但我不关心旧的 C++,也没有真正尝试过。

假设我是正确的,剩下的问题是:能否在编译时以一种符合标准且与编译器无关的方式确定 C++20 之前的系统字节顺序? None 的答案 here 似乎确实实现了这一点。他们使用 reinterpret_cast(不是编译时)、OS-header、联合别名(我相信这是 C++ 中的 UB)等。此外,出于某种原因,他们尝试“在 运行time" 尽管编译后的可执行文件将始终 运行 在相同的字节序下。)

可以在 constexpr 上下文之外执行此操作,并希望它被优化掉。另一方面,可以使用系统定义的预处理器定义并考虑所有平台,这似乎是 Boost 所采用的方法。或者也许(虽然我猜另一种方法更好?)使用宏并从网络库中选择平台特定的 htnl 风格的函数(完成,例如 here (GitHub))?

compile time-enabled solution.

首先考虑这是否是有用的要求。该程序不会在编译时与另一个系统通信。在什么情况下您需要在编译时常量上下文中使用序列化整数?

  1. Starting at what C++ standard is there a portable standard-compliant way of performing host to network byte order conversion?

从 C++98 开始,可以用标准 C++ 编写这样的函数。也就是说,后来的标准带来了美味的模板,使它变得更好。

最新标准的标准库中没有这个函数

  1. Should such a pure-c++ solution be preferred to OS specific functions such as, e.g., POSIX-htonl? (I think yes)

POSIX 的优点是编写测试以确保其正常工作不太重要。

纯 C++ 函数的优势在于,对于那些不符合 POSIX.

的平台,您不需要特定于平台的替代方案

此外,POSIX htonX 仅适用于 16 位和 32 位整数。您可以改用某些 *BSD 和 Linux (glibc) 中的 htobeXX 函数。


这是我自 C+17 以来一直在使用的。事前的一些注意事项:

  • 由于字节序转换总是1 以进行序列化,因此我将结果直接写入缓冲区。转换为主机字节序时,我从缓冲区读取。

  • 我不使用 CHAR_BIT 因为网络无论如何都不知道我的字节大小。网络字节是一个八位字节,如果您的 CPU 不同,那么这些功能将不起作用。正确处理 non-octet 字节是可能的,但不必要的工作,除非您需要在此类系统上支持网络通信。添加断言可能是个好主意。

  • 我更喜欢称它为大端而不是“网络”端。 reader 有可能不知道 de-facto 网络字节序很大的约定。

  • 与其检查“如果本机字节顺序是 X,则执行 Y,否则执行 Z”,我更愿意编写一个适用于所有本机字节顺序的函数。这可以通过位移来完成。

  • 是的,它是 constexpr。不是因为它需要,而是因为它可以。我无法举出一个示例,其中删除 constexpr 会产生更糟糕的代码。

    // helper to promote an integer type
    template <class T>
    using promote_t = std::decay_t<decltype(+std::declval<T>())>;
    
    template <class T, std::size_t... I>
    constexpr void
    host_to_big_impl(
        unsigned char* buf,
        T t,
        [[maybe_unused]] std::index_sequence<I...>) noexcept
    {
        using U = std::make_unsigned_t<promote_t<T>>;
        constexpr U lastI = sizeof(T) - 1u;
        constexpr U bits = 8u;
        U u = t;
        ( (buf[I] = u >> ((lastI - I) * bits)), ... );
    }
    
    
    template <class T, std::size_t... I>
    constexpr void
    host_to_big(unsigned char* buf, T t) noexcept
    {
        using Indices = std::make_index_sequence<sizeof(T)>;
        return host_to_big_impl<T>(buf, t, Indices{});
    }
    

1 在我遇到的所有用例中。从整数到整数的转换可以通过委托来实现,如果你有这种情况,尽管由于需要 reinterpret_cast.

它们不能是 constexpr

我做了一个基准比较问题中的 C++ 解决方案和 eeroika 接受的答案中的解决方案。

看这个完全是浪费时间,但既然我做了,我想还是分享一下吧。结果是(在我查看的特定 not-quite-realistic 用例中)它们在性能方面似乎是等效的。尽管我的解决方案被编译为使用 x86-bswap,而 eeroika 的解决方案仅使用 mov.

当使用不同的编译器时,性能似乎有很大差异 (!!),我从这些基准测试中学到的主要内容是,我只是在浪费时间...

// benchmark to compare two C++20-stand-alone host-to-big-endian endianess conversion.]
// Run at quick-bench.com! This is not a complete program. (https://quick-bench.com/q/2qnr4xYKemKLZupsicVFV_09rEk)
// To run locally, include Google benchmark header and a main method as required by the benchmarking library.
// Adapted from 
#include <type_traits>
#include <utility>
#include <cstddef>
#include <cstdint>
#include <climits>
#include <type_traits>
#include <utility>
#include <bit>
#include <random>

/////////////////////////////// Solution 1 ////////////////////////////////

template <typename T> struct scalar_t { T t{}; /* no begin/end */ };
static_assert(not std::ranges::range< scalar_t<int> >);

template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) noexcept {
  constexpr auto bits_per_byte = 8u;
  static_assert(bits_per_byte == CHAR_BIT);
  return ((((i >> (N * bits_per_byte)) & (T)(unsigned char)(-1)) <<
           ((sizeof(T) - 1 - N) * bits_per_byte)) | ...);
}; //                                             ^~~~~ fold expression

template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) noexcept {
  return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}

constexpr inline auto host_to_net(std::integral auto i) {
    static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
    if constexpr (std::endian::native == std::endian::big) {
        return i;
    } else {
        return bswap(i);  // replace by `std::byteswap` once it's available!
    }
}

/////////////////////////////// Solution 2 ////////////////////////////////

// helper to promote an integer type
template <class T>
using promote_t = std::decay_t<decltype(+std::declval<T>())>;

template <class T, std::size_t... I>
constexpr void
host_to_big_impl(
    unsigned char* buf,
    T t,
    [[maybe_unused]] std::index_sequence<I...>) noexcept {
    using U = std::make_unsigned_t<promote_t<T>>;
    constexpr U lastI = sizeof(T) - 1u;
    constexpr U bits = 8u;
    U u = t;
    ( (buf[I] = u >> ((lastI - I) * bits)), ... );
}


template <class T, std::size_t... I>
constexpr void
host_to_big(unsigned char* buf, T t) noexcept {
    using Indices = std::make_index_sequence<sizeof(T)>;
    return host_to_big_impl<T>(buf, t, Indices{});
}

//////////////////////// Benchmarks ////////////////////////////////////

template<std::integral T>
std::vector<T> get_random_vector(std::size_t length, unsigned int seed) {
    // NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. Don't use in production code!
    std::mt19937_64 rng{seed};
    std::uniform_int_distribution<T> distribution(
        std::numeric_limits<T>::min(), std::numeric_limits<T>::max());

    std::vector<T> result(length);
    for (auto && val : result) {
        val = distribution(rng);
    }
    return result;
}

template<>
std::vector<bool> get_random_vector<bool>(std::size_t length, unsigned int seed) {
    // NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. ONLY USE FOR TESTING!
    std::mt19937_64 rng{seed};
    std::bernoulli_distribution distribution{0.5};

    std::vector<bool> vec(length);

    for (auto && val : vec) {
        val = distribution(rng);
    }
    return vec;
}

constexpr std::size_t n_ints{1000};


static void solution1(benchmark::State& state) {
  std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
  std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());

  for (auto _ : state) {
    for (std::size_t i{}; i < intvec.size(); ++i) {
        host_to_big(buffer.data() + sizeof(int)*i, intvec[i]);
    }
    
    benchmark::DoNotOptimize(buffer);
    benchmark::ClobberMemory();
  }
}
BENCHMARK(solution1);


static void solution2(benchmark::State& state) {
  std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
  std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());

  for (auto _ : state) {
    for (std::size_t i{}; i < intvec.size(); ++i) {
        buffer[sizeof(int)*i] = host_to_net(intvec[i]);
    }
    
    benchmark::DoNotOptimize(buffer);
    benchmark::ClobberMemory();
  }
}
BENCHMARK(solution2);