No Endian Views

Document number:
P4247R0
Date:
2026-07-11
Audience:
LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Reply-to:
Jan Schultke <janschultke@gmail.com>
GitHub Issue:
wg21.link/P4247/github
Source:
github.com/eisenwave/cpp-proposals/blob/master/src/no-endian-views.cow

The design direction of [P4030R1] Endian Views should not be pursued.

Contents

1

Introduction

1.1

Endian Views are of limited use to serialization

1.1.1

The serialization pipeline

1.1.2

Contrived motivation

1.1.3

Problems with range checks

1.2

Endian Views are a pessimization

1.3

Endian Views work poorly for floating-point

2

Proposed action

3

References

1. Introduction

[P4030R1] Endian Views introduces Endianness adaptors std::views::from_little_endian, std::views::to_­little_endian, std::views::from_big_endian, and std::views::to_big_endian. I argue that these are not suitable for standardization because they are unfit for the intended use case, for the reasons described below.

1.1. Endian Views are of limited use to serialization

1.1.1. The serialization pipeline

Serialization and deserialization is a pipeline with several steps. Let's take deserialization of UTF-32 code points for example:

CONSUME       →  REINTERPRET  →  ADJUST ENDIAN
std::byte[4]     uint32_t        uint32_t

The key problem with Endian Views is that they are intended to assist in this pipeline, but they only cover the last transformation step, i.e. ADJUST ENDIAN, even though these steps are virtually always performed in one go.

1.1.2. Contrived motivation

[P4030R1] §2 Before/After Tables contains a motivating example involving

constexpr vector<uint32_t> utf16be_to_utf32be( const vector<uint16_t>& utf16be_data) { /* ... */ }

This signature is contrived because it doesn't make sense for the user to hold a vector<uint16_t> where the Endianness of the elements is incorrect (at least not if Endian Views are used); nothing can be meaningfully done with that data, other than

The last bullet is sometimes relevant in networking, where for optimization purposes, packets are held in their original Big-Endian form in memory so that data within can be serialized again without Endian swaps.

However, again, Endian Views are useless for this purpose because the use case deliberately avoids ever performing Endianness adjustment.

The comparison table the paper should be showing is:

Before After
constexpr vector<byte> utf16be_to_utf32be( const vector<byte>& utf16be_data) { return utf16be_data | views::chunk(4) | views::transform([](auto chunk) { array<byte, 4> a; ranges::copy(chunk, a.begin()); return bit_cast<uint32_t>(a); }) | views::transform([](const uint16_t x) { return endian_swap<endian::big>(x); }) | views::as_char16_t | views::to_utf32 | views::transform([](const uint32_t x) { return endian_swap<endian::little>(x); }) | views::transform([](uint32_t v) { return bit_cast<array<byte, 4>>(v); }) | views::join | ranges::to<vector>(); } constexpr vector<byte> utf16be_to_utf32be( const vector<byte>& utf16be_data) { return utf16be_data | views::chunk(4) | views::transform([](auto chunk) { array<byte, 4> a; ranges::copy(chunk, a.begin()); return bit_cast<uint32_t>(a); }) | views::from_big_endian | views::as_char16_t | views::to_utf32 | views::to_little_endian | views::transform([](uint32_t v) { return bit_cast<array<byte, 4>>(v); }) | views::join | ranges::to<vector>(); }

… where endian_swap can be trivially implemented by the user as:

template<endian E, integral T> constexpr T endian_swap(T x) { if constexpr (E != endian::native) { return byteswap(x); } else { return x; } }

This comparison table makes a few things readily apparent:

A significant improvement over the left column would look like:

constexpr vector<byte> utf16be_to_utf32be(span<const byte> utf16be_data) { return utf16be_data | views::from_bytes<char16_t, endian::big> | views::to_utf32 | views::to_bytes<endian::big> | ranges::to<vector>(); }

1.1.3. Problems with range checks

However, both with this version and all the previous ones, there is an issue with needing to check each iteration over a char32_t element whether at least four bytes remain in the range. If we didn't use views whatsoever, we could easily check whether the amount of bytes is a multiple of four once in the function, and perform the conversion in bulk unchecked for the remainder of the function.

Whether the compiler can factor out this range check is questionable, and it could significantly worsen performance of using views if it cannot. [P4030R1] does not investigate this problem.

1.2. Endian Views are a pessimization

Another major problem with Endian Views is that their intended usage is pessimized. Consider a typical use case of serializing a span<const char32_t> as UTF-32LE in a file:

constexpr void write_utf32le(FILE* file, span<const char32_t> text) { if constexpr (endian::native == endian::little) /* extremely likely */ { fwrite(text.data(), sizeof(char32_t), text.size(), file); } else { char32_t buffer[1024]; for (auto chunk : text | views::chunked(1024)) { memcpy(&buffer, chunk.data(), ranges::size(chunk) * sizeof(char32_t)); for (char32_t& x : chunk) { x = byteswap(x); } fwrite(chunk.data(), sizeof(char32_t), chunk.size(), file); } } }

Due to how widespread Little-Endian architectures are, the else branch can also arguably be omitted by most users in favor of a two-line function, where one line is:

static_assert(endian::native == endian::little);

Endian Views provide negative value for this use case because not only would it require a lot more effort to use them here, they would also drop the contiguity of the data when transforming from char32_t to an array<byte, 4>, despite the fact that Endian Views try to preserve contiguity.

Curiously, the terms performance, optimization, or throughput, do not appear once in [P4030R1] as if this subject were not worth discussing. This would not be that big of a problem

1.3. Endian Views work poorly for floating-point

The design of Endian Views is centered around integral types; from_little_endian and other view types can only be used if the element type of the transformed range is integral. However, serialization is perfectly reasonable for other types, such as std::float32_t. For example, the STL file format is essentially a sequence of 32-bit floating-point values.

With the current design, the user would have to add another two steps to the pipeline which bit-cast std::float32_t to std::uint32_t (and back) to be able to use Endian Views. This seems hostile and makes the feature even less attractive than it already is.

Even if this problem were resolved by making Endian Views accept anything trivially copyable, the more pressing issue is that not every floating-point bit pattern is valid for the type, and thus some may result in undefined behavior when returned from a function. This gives us a few options:

The first option reduces safety, the second option is unergonomic, and the third option is clunky and makes float serialization non-portable.

The last option doesn't have any of these problems, and it demonstrates that [P4030R1] artificially creates a problem that cannot be solved without unpleasant trade-offs.

2. Proposed action

Do not pursue the design of [P4030R1].

If the design is pursued, [P4030R1] needs substantial work:

That doesn't mean that the overarching problem of serialization and deserialization with Endian transformation is not worth solving. It is worth solving, but not using Endian Views.

3. References

[P4030R1] Eddie Nolan. Endian Views 2026-06-05 https://isocpp.org/files/papers/P4030R1.html