| Document #: | D4246R1 [Latest] [Status] |
| Date: | 2026-09-22 |
| Project: | Programming Language C++ |
| Audience: |
SG16 SG9 LEWGI LEWG |
| Reply-to: |
Steve Downey <sdowney2@bloomberg.net> |
<transcode>
synopsis [transcode.syn]decode_view
[transcode.custom.decode]encode_view
[transcode.custom.encode]<null_term>
synopsis [null.term.syn]null_sentinel_t
[null.term.sentinel]null_term_view
[null.term.view]views::null_term
[null.term.adaptor]“It seems like a really messy situation and you’re proposing to inherit that mess into the C++ standard.” — Jan Schultke [Schultke]
He is right about the mess. The encodings are underspecified, the implementations disagree about the edge cases, and no amount of standards work will make Shift_JIS pleasant. However, the data exists, and C++ programs have to read it. The alternative to a specified transcoder is the world we have, where every program invents its own recovery rules and gets different answers from the same bytes. WHATWG has already done the work of pinning down what every byte means. What this proposal inherits is that work.
We propose a set of transcoding facilities for text between character
encodings, centered on separate WHATWG decode and encode adaptors, eager
bulk decode and encode helpers, a convenience composed transcoder, and
an iconv-based adaptor for broader
encoding support.
The views are lazy and compose in pipelines: decode byte sequences to Unicode scalar values, encode Unicode back to bytes. The eager bulk operations collect into containers or write through output iterators, for when a concrete result is more convenient than a view pipeline.
Codec semantics follow the WHATWG Encoding Standard [WHATWG-Encoding]:
the same byte-to-scalar mappings and the same error recovery the
browsers implement. For encodings not covered by WHATWG, an
iconv-based adaptor provides access
to the platform’s native transcoding capabilities where the
implementation supplies that facility. The reference implementation
currently requires iconv at
package-configuration time, even though its WHATWG implementation is
otherwise portable header-only source.
The wording in this revision is generated from the reference
implementation’s headers, by a tool that reads the shipping declarations
and renders them as draft clauses. It is not a transcription, and it is
not maintained alongside the code: a header that changes without the
wording changing with it fails the implementation’s CI. The check is a
make target, so a reviewer need not
take the claim on faith.
iconv adaptor is specified
rather than promised:
iconv_transcode and
iconv_transcode_or_error over
caller-provided staging storage, with [transcode.iconv] written last and
resting on nothing the other clauses need.decode_to /
encode_to /
decode_into /
encode_into, each specified as the
pipeline it names.transcode_string, for
runtime-selected transcoding by codec or by label.sniff_encoding
examines the stream when the codec is not yet known._or_error view pairs
collapsed into one view template parameterized on
transcode_error_kind.whatwg_error and P2728’s
utf_transcoding_error.IANA registered names for character sets before anyone had specified
what the names meant. The registry says a document is “Shift_JIS”; it
does not say what
0x81 0x40
decodes to. C answered with the multibyte functions and POSIX
standardized iconv, and both work,
as long as you do not care which implementation you get. glibc, musl,
and the BSD and macOS libraries disagree about error recovery and about
which encodings exist at all, and all of them depend on how the system
was configured. C++ added
std::codecvt
in
<locale>,
and the
<codecvt>
header’s Unicode conversion facets on top of it.
<codecvt>
— codecvt_utf8,
codecvt_utf16,
codecvt_utf8_utf16 — was deprecated
in C++17 and removed in C++26 [P2871R3], and the
char16_t and
char32_t
facet specializations deprecated in C++20 went with it [P2873R3].
std::codecvt
itself is still in
<locale>;
what was removed is everything in the pair that converted Unicode. It
allocated on every call and dispatched through a virtual interface to do
so; in the accompanying benchmark, the C89
mbstowcs is faster.
The WHATWG Encoding Standard specified the missing part, for exactly
the encodings the web still carries. It gives the byte-to-scalar mapping
for every byte value, says what each malformed sequence produces, and
the Web Platform Tests [WPT-Encoding]
pin all of it across the browser engines. C++ has since added
std::text_encoding,
which names an encoding. Nothing in the standard converts one.
Everyone should be using UTF-8. Postel’s law applies anyway: conservative in what we emit, liberal in what we accept. There is a great deal of existing data in legacy encodings that backend C++ systems have to read, web interchange has not finished migrating, and email still generates and routes large volumes of non-UTF-8 text.
Real data is also badly formed. Emails and web pages lie about their declared encodings, and concatenate mismatched encodings into a single payload. So the facility has to keep going past a bad byte and let the caller decide what that meant. Aborting on the first bad byte is not an option when the bytes are what you were sent.
Before: manual loop
|
After:
whatwg_decode
|
|---|---|
|
|
Before: iconv
|
After: decode then encode
|
|---|---|
|
|
Before: mbstowcs
|
After:
views::null_term
|
|---|---|
|
|
Before: manual tracking
|
After: _or_error
|
|---|---|
|
|
Before: manual collection loop
|
After: decode_to
|
|---|---|
|
|
We propose decode_to /
encode_to /
decode_into /
encode_into, and they add nothing
the views do not already do. The views satisfy the range concepts that
std::ranges::to
and
std::ranges::copy
require, so the standard algorithms already do the job, and there is no
measurable difference in speed between a named
decode_to and the equivalent
view | ranges::to
composition.
The names are worth having anyway. Converting a buffer to a container
is the operation users come for, and
decode_to<codec::utf_8>(input)
is what they look for first. Withholding the name only moves the
question to every code review and every committee discussion of this
facility. Each helper is a one-line forward to the pipeline it names, so
the cost is four names and the wording that specifies them.
The named helpers and pipelines are the same operations:
decode_to<C>(r) is r | whatwg_decode<C> | ranges::to<vector<char32_t>>()
encode_to<C, Cont>(r) is r | whatwg_encode<C> | ranges::to<Cont>()
decode_into<C>(r, out) is ranges::copy(r | whatwg_decode<C>, out)
encode_into<C>(r, out) is ranges::copy(r | whatwg_encode<C>, out)mbstowcs,
iconv, and
std::codecvt
are covered above, and the comparison tables show what each of them
costs at the call site. The remaining option is to take a dependency:
ICU, Boost.Text, and the like are capable, and heavyweight enough that
not every project can afford one.
None of the four integrate with the ranges library. Converting between encodings requires extracting data from one container, passing it through a conversion API, and collecting into another container — losing the composability that makes ranges effective.
The WHATWG Encoding Standard defines precise behavior for every legacy encoding encountered on the web. It gives exact byte-to-scalar mappings for all byte values, including every error case, and the Web Platform Tests [WPT-Encoding] pin that behavior across all major browsers.
The objection is real. WHATWG is a web specification, and it makes
web-compatible choices that a general text library would not. It
conflates distinct UTF-8 error conditions, it strips BOMs, and its label
table exists to parse HTML
<meta>
tags. However, those choices are the ones that major browser engines
already agree on and test, for the encodings that legacy data is
actually written in. A general specification with no implementations to
agree with would be worse.
Targeting it means a C++ program decodes a page the way the browser that fetched it did, and parses HTML and JSON with the same error handling.
The proposed decode, encode, and
iconv transcode views all model
input_range. That is the strongest
category that can be implemented uniformly across all supported
encodings, and the stateful multibyte codecs are what hold it there. The
WHATWG decode and encode views preserve stronger properties when the
codec and base range permit it: const-qualified iteration is supported
for const-compatible bases,
common_range and
borrowed_range propagate from the
base when implementable, and indexed codecs lift to
random_access_range. The eager bulk
helpers are layered on top of those pipelines when callers want
collected results.
Legacy multibyte encodings like Shift_JIS, Big5, and GB18030 are fundamentally forward-only. Unlike UTF-8, where lead and continuation bytes occupy disjoint ranges, legacy encodings have overlapping byte values that can serve as either lead or trail bytes depending on context.
Consider Shift_JIS: the byte
0x81 is
valid both as a lead byte starting a two-byte sequence and as a trail
byte completing one. Given only a pointer into the middle of a byte
stream, it is impossible to determine whether you are at a character
boundary without scanning from the beginning.
Even with a known character boundary, stepping backward is impossible
without external state. Take the byte sequence
0x81 0x81 0x81 0x40.
Read from a character boundary it is
0x81 0x81
and
0x81 0x40:
= (U+FF1D FULLWIDTH EQUALS SIGN) then (U+3000 IDEOGRAPHIC SPACE).
Read one byte late — the first
0x81 having
been the trail byte of a character that began before it — what is left
is
0x81 0x81
and a lone
0x40: =
then ‘@’. Same four bytes, different text, and which reading applies
depends on how many
0x81 bytes
preceded them from the string’s start.
WHATWG’s state machines are designed as forward-only decoders, resetting their state at each character boundary, and bidirectional iteration would require caching all byte offsets during forward traversal. The encodings are like that. The implementation has no choice.
The proposal adds four user-facing adaptor families:
whatwg_decode_view<C>
/
whatwg_decode<C>:
Decodes a byte range to
char32_t
(Unicode scalar values). Decode errors are replaced with
U+FFFD.
whatwg_encode_view<C>
/
whatwg_encode<C>:
Encodes a
char32_t
range to bytes. Input is validated as UTF-32: an ill-formed code unit is
replaced with U+FFFD, then encoded through
C. A scalar value that
C cannot represent is replaced with
'?'.
The WHATWG UTF-16BE and UTF-16LE encodings are decode-only in this
interface, matching the Encoding Standard’s lack of UTF-16BE/LE
encoders.
whatwg_decode_or_error<C>
/
whatwg_encode_or_error<C>:
Error-reporting variants yielding
expected<T, whatwg_error>.
These are adaptor objects rather than separate view templates: each is
the view above with a different
transcode_error_kind, so
whatwg_decode_or_error<C>
produces
whatwg_decode_view<C, R, transcode_error_kind::expected>.
iconv_transcode_view
/
iconv_transcode(from, to, buf)
and
iconv_transcode_or_error_view
/
iconv_transcode_or_error(from, to, buf):
Runtime-selected transcoding through the platform
iconv implementation, using
caller-provided staging storage.
Four eager bulk helpers, each specified as the pipeline it names:
decode_to<C>(r)
/
encode_to<C, Container>(r):
Collect the decoded or encoded result into a container.
decode_into<C>(r, out)
/
encode_into<C>(r, out):
Write the result through an output iterator.
And three utility entry points:
transcode<From, To>:
Convenience composition of
whatwg_decode<From>
followed by
whatwg_encode<To>,
for To codecs that have a WHATWG
encoder.
transcode_string(source, from, to):
Eager convenience helper for runtime-selected WHATWG transcoding by
codec or by label. It returns an engaged
optional<string>
containing the converted bytes, including an empty string for an empty
input, or nullopt if the target
codec has no WHATWG encoder.
null_term_view /
views::null_term:
Adapts a pointer to a null-terminated string into a range.
views::null_term(cstr) | whatwg_decode<codec::utf_8>
then composes.
enum class codec {
utf_8,
// WHATWG special
replacement, // Always yields a single U+FFFD
x_user_defined, // Maps 0x80-0xFF to U+F780-U+F7FF
// Single-byte legacy
ibm866, iso_8859_2, iso_8859_3, iso_8859_4,
iso_8859_5, iso_8859_6, iso_8859_7, iso_8859_8,
iso_8859_8_i, iso_8859_10, iso_8859_13, iso_8859_14,
iso_8859_15, iso_8859_16, koi8_r, koi8_u, macintosh,
windows_874, windows_1250, windows_1251, windows_1252,
windows_1253, windows_1254, windows_1255, windows_1256,
windows_1257, windows_1258, x_mac_cyrillic,
utf_16be, // decode-only: WHATWG defines no UTF-16BE encoder
utf_16le, // decode-only: WHATWG defines no UTF-16LE encoder
// CJK multibyte
gbk,
gb18030,
big5,
shift_jis,
euc_jp,
iso_2022_jp,
euc_kr,
};The enumerator order is the one [transcode.codec] specifies, comments aside. This paper does not fix the enumerators’ values, but it does fix their order, so the two have to agree.
The codec is a template parameter and not a runtime value, so there is no per-element dispatch, and transcoding can run at compile time.
The WHATWG Encoding Standard §4.2 defines over 200 string labels that
map to canonical encodings. For example,
"shift_jis",
"sjis",
"x-sjis",
"ms_kanji",
and
"csshiftjis"
all refer to the same Shift_JIS codec. This mapping is essential
for:
charset attributes in
HTML
<meta>
tagscharset parameters in
Content-Type
headersA get_encoding function provides
runtime codec selection:
constexpr std::optional<codec> get_encoding(std::string_view label) noexcept;The function performs ASCII case-insensitive matching and strips leading/trailing ASCII whitespace, per WHATWG §4.2:
static_assert(get_encoding("UTF-8") == codec::utf_8);
static_assert(get_encoding("utf-8") == codec::utf_8);
static_assert(get_encoding(" utf-8 ") == codec::utf_8);
static_assert(get_encoding("shift_jis") == codec::shift_jis);
static_assert(get_encoding("SJIS") == codec::shift_jis);
static_assert(get_encoding("x-sjis") == codec::shift_jis);
static_assert(get_encoding("unknown") == std::nullopt);Runtime dispatch on external metadata follows:
std::string_view charset = parse_content_type(headers);
if (auto c = get_encoding(charset)) {
// Use std::visit or switch to dispatch to appropriate view
}Two WHATWG error handling strategies are provided:
Replacement mode
(whatwg_decode,
whatwg_encode, and
transcode): Invalid byte sequences
decode to U+FFFD. Ill-formed UTF-32 input is replaced with U+FFFD before
encoding, while a scalar value the target encoding cannot represent is
replaced with
'?'.
This follows the WHATWG replacement model and is appropriate for most
text processing where halting on errors is undesirable.
Inspection mode
(whatwg_decode_or_error,
whatwg_encode_or_error): Each output
element is
expected<T, whatwg_error>.
This allows callers to log errors, apply custom replacement, or abort
processing.
For WHATWG codecs, the error enumeration provides specific failure reasons:
enum class whatwg_error {
invalid_byte, // Byte not valid in this position
truncated_sequence, // End of input mid-character
overlong_encoding, // UTF-8 overlong sequence
surrogate_code_point,// Decoded or UTF-32 input value is a surrogate
out_of_range, // Decoded or UTF-32 input value is above U+10FFFF
unmapped_codepoint, // Encode: no mapping exists
};utf_transcoding_error (P2728)P2728 (beman.utf_view) defines
utf_transcoding_error for its
_or_error views. The two
enumerations are distinct types. Most values correspond directly:
whatwg_error
|
utf_transcoding_error
(P2728)
|
Notes
|
|---|---|---|
truncated_sequence |
truncated_utf8_sequence |
Same concept; whatwg_error
applies to all codecs, not just UTF-8 |
overlong_encoding |
overlong |
Identical semantics |
surrogate_code_point |
encoded_surrogate |
Identical semantics for UTF-8 and UTF-32 |
out_of_range |
out_of_range |
Identical semantics |
invalid_byte |
invalid_utf8_leading_byte +
unexpected_utf8_continuation_byte |
P2728 distinguishes two invalid-byte cases; WHATWG conflates them because its spec produces U+FFFD for both without differentiating |
unmapped_codepoint |
(no equivalent) | See below |
| (not needed) | unpaired_high_surrogate /
unpaired_low_surrogate |
UTF-16 code-unit-level concepts; this library operates on byte
streams with explicit endianness
(codec::utf_16be/utf_16le),
so there is no paired/unpaired distinction at the byte level |
unmapped_codepoint is the reason
reusing utf_transcoding_error
directly is not possible. P2728’s Unicode-only encoders can encode every
Unicode scalar value, so P2728 never needs to report “no mapping exists
for this code point.” Legacy codecs cannot: windows-1252 has no
representation for U+4E2D (中), Shift-JIS has no representation for
U+00E9 (é), and so on for most of the Unicode range.
whatwg_encode_or_error<codec::windows_1252>
must be able to report this condition through the same error type as
whatwg_encode_or_error<codec::utf_8>,
so the error type must accommodate it uniformly across the WHATWG encode
surface.
The iconv adaptors use a separate
iconv_error enumeration:
enum class iconv_error {
invalid_sequence,
incomplete_sequence,
output_full,
open_failed,
system_error,
};Two error vocabularies in one header wants a defence, so here it is.
They are separate because the failures are not the same failures.
whatwg_error names what the Encoding
Standard says went wrong in a byte sequence this library decoded, and
every one of its enumerators corresponds to a step in an algorithm the
paper specifies. iconv_error names
what the platform reported about a conversion this library did not
perform: EILSEQ,
EINVAL,
E2BIG, a descriptor that would not
open, or an unexpected system failure. A single enumeration would have
to either drop that distinction — reporting
invalid_byte for a failure whose
meaning is “the platform’s tables say so”, with no algorithm behind it a
reader can consult — or carry both sets, which is two vocabularies with
one name. Nothing composes them, either: the
iconv adaptor is byte↔︎byte and does
not appear in a
decode | encode
pipeline, so no expression ever has to reconcile the two.
This is separate from the unification of the
_or_error views. Those pairs
collapsed into one view template parameterized on
transcode_error_kind, which selects
whether errors are reported or replaced. What an error
is remains the codec family’s own question.
Two concepts constrain the input ranges:
template <typename R>
concept legacy_byte_range =
ranges::range<R> &&
!is_array_v<remove_cvref_t<R>> &&
legacy-byte-type<remove_cv_t<ranges::range_value_t<R>>>;
template <typename R>
concept unicode_scalar_range =
ranges::input_range<R> &&
!is_array_v<remove_cvref_t<R>> &&
same_as<remove_cv_t<ranges::range_value_t<R>>, char32_t>;where legacy-byte-type
is an exposition-only concept satisfied by
char,
signed char,
unsigned char
and byte. [transcode.reqs] is the
normative text; this block restates it.
unicode_scalar_range is
intentionally a type-level concept. It identifies the library’s
scalar-value interchange representation, but it cannot prove that each
char32_t
object is a Unicode scalar value. Encode adaptors therefore interpret
their input as UTF-32 and perform the conversion to scalar values above
codec dispatch, just as the Web Platform converts strings before
invoking a WHATWG encoder. Surrogates and values above U+10FFFF become
U+FFFD in replacement mode or their corresponding errors in inspection
mode.
The two are not symmetric:
legacy_byte_range requires
ranges::range,
where unicode_scalar_range requires
ranges::input_range.
Nothing in the proposal reaches the weaker requirement — every view
constrained on legacy_byte_range
constrains its base on input_range
as well, and the bulk helpers forward to those views — so aligning the
two on input_range would change no
program. It is called out because the two declarations side by side
invite the question.
Raw arrays are explicitly rejected to prevent silent inclusion of
null terminators. Use
views::null_term
for null-terminated strings or wrap counted byte buffers in
span.
The
char8_t type
is not accepted because it implies UTF-8 encoding, conflicting with the
codec parameter. Similarly,
wchar_t is
rejected due to its platform-dependent size.
All views are
constexpr-enabled.
Decoding and encoding tables are compile-time constants. Decoding
therefore runs in a constant expression:
constexpr auto decoded = []() consteval {
constexpr char bytes[] = {'\xC3', '\xA9'};
std::span<const char> sp(bytes, 2);
return *(sp | whatwg_decode<codec::utf_8>).begin();
}();
static_assert(decoded == U'é');The views support both function call and pipe syntax:
// Function call
auto v1 = whatwg_decode<codec::utf_8>(bytes);
// Pipe
auto v2 = bytes | whatwg_decode<codec::utf_8>;
// Composition
auto v3 = bytes
| whatwg_decode<codec::shift_jis>
| whatwg_encode<codec::utf_8>;
// Convenience closure
auto v4 = bytes | transcode<codec::shift_jis, codec::utf_8>;All of the views are valid
input_ranges, and the WHATWG views
preserve stronger range properties where they can.
Const iteration:
whatwg_decode,
whatwg_encode, and their
_or_error variants support
begin() const
and
end() const
when the underlying range supports compatible const iteration. This
includes owning views after construction.
common_range and
borrowed_range: These
properties are preserved from the base range when the codec can support
them.
Indexed codecs: Single-byte and other indexed
codecs can provide
random_access_range behavior for
both regular and _or_error WHATWG
views. Stateful codecs such as UTF-8 and ISO-2022-JP remain forward-only
or input-only where required by their decoding model.
The separate decode and encode entry points make the pipeline direction explicit:
std::vector<char> utf8 = {'H', 'i', '\xE2', '\x82', '\xAC'};
auto code_points = utf8 | whatwg_decode<codec::utf_8>;
auto round_trip = code_points | whatwg_encode<codec::utf_8>;Collecting into owned storage uses
std::ranges::to,
or the named helpers:
std::string bytes = "caf\xE9";
auto scalars = bytes
| whatwg_decode<codec::windows_1252>
| std::ranges::to<std::vector<char32_t>>();
auto utf8 = scalars
| whatwg_encode<codec::utf_8>
| std::ranges::to<std::string>();
auto scalars2 = decode_to<codec::windows_1252>(bytes);
auto utf8_2 = encode_to<codec::utf_8, std::string>(scalars2);Writing into an output iterator uses
std::ranges::copy,
or decode_into and
encode_into:
std::vector<char32_t> scalars;
std::ranges::copy(bytes | whatwg_decode<codec::utf_8>,
std::back_inserter(scalars));
std::string encoded;
std::ranges::copy(scalars | whatwg_encode<codec::utf_8>,
std::back_inserter(encoded));
decode_into<codec::utf_8>(bytes, std::back_inserter(scalars));
encode_into<codec::utf_8>(scalars, std::back_inserter(encoded));The convenience
transcode<From, To>
adaptor is useful when the intermediate Unicode range is not otherwise
needed:
std::string latin1 = "caf\xE9";
auto utf8 = latin1
| transcode<codec::windows_1252, codec::utf_8>
| std::ranges::to<std::string>();Null-terminated inputs compose through
views::null_term:
const char* cstr = "hello";
std::u32string scalars = views::null_term(cstr)
| whatwg_decode<codec::utf_8>
| std::ranges::to<std::u32string>();Runtime labels can be resolved through WHATWG’s alias table before dispatch:
if (auto utf8 = transcode_string(bytes, "shift_jis", "utf-8")) {
consume(*utf8);
}For encodings outside the WHATWG set, the
iconv adaptor provides a
runtime-selected escape hatch:
std::array<char, 256> buffer{};
std::vector<char> utf32le = input
| iconv_transcode("UTF-8", "UTF-32LE", std::span(buffer))
| std::ranges::to<std::vector>();The legacy_byte_range concept
accepts
char,
signed char,
unsigned char,
and
std::byte —
the types that represent raw octets in C++. It explicitly rejects:
char8_t:
Implies UTF-8 encoding, which conflicts with explicit codec selection.
If you have
char8_t
data, you already know it’s UTF-8 — passing it to
whatwg_decode<codec::shift_jis>
would be a logic error.
wchar_t:
Platform-dependent width (16-bit on Windows, 32-bit on POSIX) makes it
unsuitable as an interchange format. It also implies “already decoded”
where what is wanted is “raw bytes.”
char16_t
/
char32_t:
These are Unicode code unit types. A range of
char16_t is
already decoded data that might need re-encoding.
This design reflects where encoded text comes from: network sockets
(recv returns bytes), file I/O
(read returns bytes), legacy APIs
(return
char*),
and modern buffer types
(std::byte).
The views take the bytes as they arrive, with no conversion step
first.
Rejecting
char8_t
inconveniences anyone holding a
u8string that is not in fact UTF-8,
which does happen when a codebase uses
u8string as its default string type.
However, accepting it would mean accepting a type whose purpose is to
state an encoding, and then ignoring what it states. Such a caller can
use as_bytes and make that
reinterpretation visible.
// All of these work:
std::vector<char> v1;
std::vector<unsigned char> v2;
std::vector<std::byte> v3;
std::span<const signed char> s;
v1 | whatwg_decode<codec::utf_8>; // OK
v2 | whatwg_decode<codec::utf_8>; // OK
v3 | whatwg_decode<codec::utf_8>; // OK
s | whatwg_decode<codec::utf_8>; // OK
// These are rejected at compile time:
std::u8string u8s;
u8s | whatwg_decode<codec::utf_8>; // Error: char8_t implies UTF-8Making the codec a template parameter buys inlined codec logic, no
dispatch per element, and
constexpr
transcoding.
The cost is real, and it is the common case: the encoding usually
arrives as a string from an HTTP header or a
<meta>
tag, and a template parameter cannot be spelled from a runtime string.
However, the dispatch has to happen exactly once, at the point where the
label is resolved, and it is a
switch over
an enumeration. get_encoding and
transcode_string provide that
dispatch, and runtime selection can be layered further via
variant or
any. Paying for dispatch on every
code point instead would give up
constexpr
decoding for all callers to spare one
switch for
some of them.
Unicode scalar values are represented as single
char32_t
values. This simplifies composition, since a
char32_t
stream can be fed to any encoder without reparsing, and makes individual
code point inspection trivial.
The type is an interchange representation. Nothing about
char32_t
validates what it holds. Decode views produce scalar values before an
encoder sees them; a range of
char32_t
supplied from outside a decode pipeline is validated as UTF-32 by the
encode view. Ill-formed code units are replaced with U+FFFD or reported,
according to the view’s error kind, before the selected codec is
invoked.
UTF-32 is the wasteful choice. A
char32_t per
scalar is four bytes where UTF-8 would often use one. However, these are
lazy views, so nothing is stored unless a caller collects it, and a
single code point is trivial to inspect. All other interchange types are
worse.
Rather than a single primary “transcode from A to B” adaptor, decode and encode are separate operations composed via pipes. This enables:
bytes | whatwg_decode<codec::utf_8> | filter(...)codepoints | tee(whatwg_encode<codec::utf_8>(...), whatwg_encode<codec::shift_jis>(...))char32_t,
encode consumes
char32_tThe
transcode<From, To>
convenience closure is layered on top of this composition model for the
common case where the intermediate
char32_t
stream is not observed. The eager bulk helpers are layered on the same
decode and encode semantics, but package the common collection patterns
directly.
The WHATWG UTF decoders strip a leading BOM when it matches the selected codec. This matches browser behavior and the Web Platform Tests [WPT-Encoding] for UTF-8, UTF-16LE, and UTF-16BE.
When the codec is not yet known, a separate
sniff_encoding helper can examine
the byte stream for a BOM before dispatch:
if (auto codec = sniff_encoding(bytes)) {
// Dispatch according to the detected BOM.
}Non-matching BOMs are not stripped. For example, a UTF-8 decoder does not remove a UTF-16LE BOM prefix.
Unicode normalization (NFC, NFD, NFKC, NFKD) is orthogonal to
encoding. A
normalize_view<form>
is an obvious future addition, but not part of this proposal.
The reference implementation does not hand-transcribe encoding tables or conformance fixtures. Instead, it stores pristine upstream source material for both the WHATWG Encoding Standard indexes [WHATWG-Encoding] and the relevant Web Platform Tests (WPT) [WPT-Encoding] under version control, together with provenance metadata and checksums.
Keeping the upstream bytes ties the implementation to the same normative and de facto-interoperability sources that define browser behavior. It also makes source updates auditable during review, because regenerated artifacts can be compared against the exact upstream inputs that produced them. And it avoids network access during normal builds and test runs; regeneration is an explicit maintenance step rather than a hidden part of compilation.
The project uses small Python helpers to transform upstream WHATWG and WPT data into C++-friendly artifacts. The generators are checked in.
For codec tables, Python scripts download the WHATWG index files, record provenance, and generate checked-in lookup tables for single-byte and multibyte codecs. This avoids manually maintaining large arrays of code points while keeping the generated results reviewable and deterministic.
For conformance tests, a separate Python generator parses the JavaScript-authored WPT fixtures and emits C++ headers containing byte sequences, expected Unicode scalar values, and descriptive labels. That translation step is necessary because the authoritative tests are written for the web platform rather than for C++. By converting them once into ordinary C++ data, the test suite can run as native unit tests without embedding a JavaScript engine or reimplementing the WPT harness at runtime.
The generators are themselves unit-tested. That keeps the extraction
logic honest: if WPT uses surrogate pairs, replacement characters,
BOM-sensitive cases, fatal-mode expectations, or codec-specific fixture
shapes, those parsing rules are validated independently from the
transcoder under test. For the
USVString surrogate vectors, the
Python parser combines surrogate pairs into one
char32_t and
leaves lone surrogates for the C++ view’s UTF-32 validation.
Consequently the astral vector is not a surrogate-validation case in
C++, and a
char32_t
range cannot model a pair split across chunks.
The implementation is validated against the WHATWG Web Platform Tests (WPT) [WPT-Encoding] for text encoding. These tests provide:
USVString coercion test rather than
from a WHATWG encoder algorithmTest vectors are extracted from WPT and converted to C++ data structures using the preprocessing pipeline described in Methods. The implementation is checked against browser behavior automatically, with the executable tests entirely within the native C++ test harness.
Every concept constraint has a corresponding negative compile test —
a .cpp file
that must fail to compile with a specific diagnostic. This ensures:
char8_t
ranges are rejected (wrong for explicit codec selection)wchar_t
ranges are rejected (non-portable width)Every operation that should be
constexpr
has a
consteval
test verifying compile-time evaluation:
constexpr auto result = []() consteval {
constexpr char bytes[] = {'\xE2', '\x82', '\xAC'};
std::span<const char> sp(bytes, 3);
return *(sp | whatwg_decode<codec::utf_8>).begin();
}();
static_assert(result == U'€');For codecs where encode is defined, roundtrip tests verify that
encode(decode(bytes)) == bytes
for all valid inputs, and that
decode(encode(codepoints)) == codepoints
for all mapped code points.
Bulk collection through
ranges::to
and output-iterator paths via
ranges::copy
are covered alongside the named helpers that forward to them.
This proposal adds new headers and does not modify existing standard library components. It is a pure extension.
#define __cpp_lib_transcode_view 202XXXL#include <transcode> // whatwg_decode, whatwg_encode, transcode, iconv_transcode, transcode_string
#include <null_term> // null_term_viewOr combined into
<ranges>
additions.
The random-access custom codec protocol reserves U+FFFD as the
in-band signal that a byte
0x80 or
above decodes to nothing. That keeps
decode_byte cheap, but it prevents a
custom codec from mapping such a byte to the actual U+FFFD scalar:
replacement mode returns U+FFFD, while error-reporting mode calls the
same value invalid_byte. Should the
protocol retain that reservation? The alternatives are to return a
result type from decode_byte, add a
separate validity query, or keep and document the present restriction.
This paper does not choose between them before SG16 review: no WHATWG
single-byte codec needs the foreclosed mapping, and changing the indexed
path for a hypothetical extension has a cost that the group should weigh
explicitly.
The decisions embodied in the design, and in the wording that follows:
transcode<From, To>
is the composed convenience for when the intermediate
char32_t
stream is not observed.get_encoding and
transcode_string.char32_t
is the interchange type. Nothing about it validates what it
holds, so a
char32_t
range from outside a decode pipeline is validated as UTF-32 by the
encode view.char,
signed char,
unsigned char,
std::byte.
char8_t,
wchar_t and
raw arrays do not participate.whatwg_error and
iconv_error, because the failures
are not the same failures. Whether an error is reported or replaced is
one view template parameterized on
transcode_error_kind.input_range and preserve
more where the codec and base permit, up to
random_access_range for indexed
codecs.The iconv adaptor is proposed
here, with the portable WHATWG facilities, rather than on its own. Its
conversion-descriptor handle cannot be named entirely in ISO C++ terms
without either introducing an opaque handle abstraction or moving the
adaptor to a POSIX-conditional document. It is useful implementation
experience and a valuable comparison point, and the shared interface is
best considered as a whole, so it belongs here through SG16 design
review. Its wording is [transcode.iconv], written last and resting on
nothing the other clauses need, so splitting it later renumbers
nothing.
The objection to specifying it is that POSIX
iconv is implementation-defined
across glibc, musl and the BSDs, so a view over it specifies whatever
the platform does. That is true, and the wording does not pretend
otherwise: what it specifies is what the adaptor guarantees —
one conversion descriptor per begin,
closed by the iterator that owns it, and input the conversion refuses
either skipped or surfaced as an
iconv_error — and it leaves the
conversion to the implementation, as
<locale>
leaves a locale’s tables to it. That guarantee is the reason to propose
it at all: the C interface hands a program a descriptor it must remember
to close, and an adaptor is where that stops being the program’s
problem.
These clauses are generated, as the abstract says, and transcluded into this paper by its build. The generator, the marked-up headers and the drift check are all in the repository cited in References; a reviewer can regenerate this section and diff it.
Wording is relative to the current working draft.
Add the headers
<transcode>
and
<null_term>
to the table of C++ library headers in [headers].
Add the following to the normative references in [intro.refs]:
— WHATWG, Encoding Standard, available at https://encoding.spec.whatwg.org/.
[Note: the Encoding Standard is a Living Standard, so a
normative reference to it has to name what it is a reference
to. The wording below is written against commit
a985b62a9b45c17da3e17a9f0a0b4e30c34c4a8a
(21 May 2026), the commit cited in [WHATWG-Encoding],
and the reference implementation’s codec tables and conformance vectors
are generated from that same snapshot. Whether the standard cites a
dated snapshot, a commit, or the living document is an editorial
question this paper puts to the project editor rather than answers. —
end note]
Add the following macro to [version.syn], in the place the table’s alphabetical order puts it:
#define __cpp_lib_transcode_view 202XXXL // also in <transcode>, <null_term>Add a new clause [transcode], “Text transcoding”, as follows.
[Editor’s note: the clause headings below carry their stable
names.
[transcode.syn]
and
[null.term.syn]
are the two header synopses; every other clause specifies what one of
them declares. — end note]
<transcode>
synopsis [transcode.syn]// [transcode.errors], error types
enum class whatwg_error {
invalid_byte,
truncated_sequence,
overlong_encoding,
surrogate_code_point,
out_of_range,
unmapped_codepoint,
};
enum class iconv_error {
invalid_sequence,
incomplete_sequence,
output_full,
open_failed,
system_error,
};
enum class transcode_error_kind {
replacement,
expected,
};
// [transcode.reqs], range requirements
template<typename T>
concept legacy-byte-type =
same_as<T, char> || same_as<T, signed char> || same_as<T, unsigned char> ||
same_as<T, byte>; // exposition only
template<typename R>
concept legacy_byte_range = ranges::range<R> && !is_array_v<remove_cvref_t<R>> &&
legacy-byte-type<remove_cv_t<ranges::range_value_t<R>>>;
template<typename R>
concept unicode_scalar_range =
ranges::input_range<R> && !is_array_v<remove_cvref_t<R>> &&
same_as<remove_cv_t<ranges::range_value_t<R>>, char32_t>;
// [transcode.codec], encodings
enum class codec {
utf_8,
replacement,
x_user_defined,
ibm866,
iso_8859_2,
iso_8859_3,
iso_8859_4,
iso_8859_5,
iso_8859_6,
iso_8859_7,
iso_8859_8,
iso_8859_8_i,
iso_8859_10,
iso_8859_13,
iso_8859_14,
iso_8859_15,
iso_8859_16,
koi8_r,
koi8_u,
macintosh,
windows_874,
windows_1250,
windows_1251,
windows_1252,
windows_1253,
windows_1254,
windows_1255,
windows_1256,
windows_1257,
windows_1258,
x_mac_cyrillic,
utf_16be,
utf_16le,
gbk,
gb18030,
big5,
shift_jis,
euc_jp,
iso_2022_jp,
euc_kr,
};
// [transcode.codec.label], label lookup
constexpr optional<codec> get_encoding(string_view label) noexcept;
// [transcode.custom.reqs], codec requirements
struct decode_result {
char32_t code_point{};
whatwg_error error{};
bool is_error{false};
};
struct encode_result {
unsigned char bytes[8]{};
int count{0};
bool is_error{false};
};
// [transcode.custom.reqs], codec requirements
template<typename C>
concept decode_codec = semiregular<C> && requires(C& c, const unsigned char*& iter,
const unsigned char* sentinel) {
{ c.decode_one(iter, sentinel) } -> same_as<decode_result>;
};
template<typename C>
concept random_access_decode_codec_type =
decode_codec<C> && requires(const C& c, unsigned char byte) {
{ c.decode_byte(byte) } -> same_as<char32_t>;
};
template<typename C>
concept encode_codec = semiregular<C> && requires(C& c, char32_t cp) {
{ c.encode_one(cp) } -> same_as<encode_result>;
};
template<typename C>
concept flushable_decode_codec = decode_codec<C> && requires(C& c) {
{ c.flush() } -> same_as<optional<decode_result>>;
};
// [transcode.whatwg.decode], decoding views
template<codec C, ranges::input_range R,
transcode_error_kind E = transcode_error_kind::replacement>
requires legacy_byte_range<R>
class whatwg_decode_view : public ranges::view_interface<whatwg_decode_view<C, R, E>> {
R base; // exposition only
class iterator; // exposition only
public:
// [transcode.whatwg.decode], construction and access
constexpr explicit whatwg_decode_view(R base);
constexpr const R& base() const& noexcept;
constexpr R base() &&;
constexpr iterator begin();
constexpr iterator begin() const
requires const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr iterator end()
requires ranges::forward_range<R> && ranges::common_range<R>;
constexpr iterator end() const
requires ranges::forward_range<const R> && ranges::common_range<const R> &&
const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr default_sentinel_t end() const;
};
template<codec C> inline constexpr unspecified whatwg_decode;
template<codec C> inline constexpr unspecified whatwg_decode_or_error;
template<codec C, input_range R, transcode_error_kind E>
requires legacy_byte_range<R>
inline constexpr bool enable_borrowed_range<whatwg_decode_view<C, R, E>> =
borrowed_range<R>;
// [transcode.whatwg.encode], encoding views
template<codec C, ranges::input_range R,
transcode_error_kind E = transcode_error_kind::replacement>
requires whatwg-encode-input<C, R>
class whatwg_encode_view : public ranges::view_interface<whatwg_encode_view<C, R, E>> {
R base; // exposition only
class iterator; // exposition only
public:
// [transcode.whatwg.encode], construction and access
constexpr explicit whatwg_encode_view(R base);
constexpr const R& base() const& noexcept;
constexpr R base() &&;
constexpr iterator begin();
constexpr iterator begin() const
requires const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr iterator end()
requires ranges::forward_range<R> && ranges::common_range<R>;
constexpr iterator end() const
requires ranges::forward_range<const R> && ranges::common_range<const R> &&
const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr default_sentinel_t end() const;
};
template<codec C> inline constexpr unspecified whatwg_encode;
template<codec C> inline constexpr unspecified whatwg_encode_or_error;
template<codec C, input_range R, transcode_error_kind E>
requires whatwg-encode-input<C, R>
inline constexpr bool enable_borrowed_range<whatwg_encode_view<C, R, E>> =
borrowed_range<R>;
// [transcode.custom.decode], class template decode_view
template<decode_codec Codec, ranges::input_range R,
transcode_error_kind E = transcode_error_kind::replacement>
requires legacy_byte_range<R>
class decode_view : public ranges::view_interface<decode_view<Codec, R, E>> {
R base; // exposition only
Codec codec; // exposition only
class iterator; // exposition only
public:
// [transcode.custom.decode], construction and access
constexpr explicit decode_view(R base, Codec codec = {});
constexpr const R& base() const& noexcept;
constexpr R base() &&;
constexpr iterator begin();
constexpr iterator begin() const
requires const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr iterator end()
requires ranges::forward_range<R> && ranges::common_range<R>;
constexpr iterator end() const
requires ranges::forward_range<const R> && ranges::common_range<const R> &&
const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr default_sentinel_t end() const;
};
template<decode_codec Codec> constexpr decode_closure<Codec> decode(Codec codec = {});
template<decode_codec Codec>
constexpr decode_closure<Codec, transcode_error_kind::expected> decode_or_error(
Codec codec = {});
template<decode_codec Codec, ranges::input_range R>
decode_or_error_view decode_or_error_view =
decode_view<Codec, R, transcode_error_kind::expected>;
template<random_access_decode_codec_type Codec, ranges::random_access_range R>
random_access_decode_or_error_view random_access_decode_or_error_view =
random_access_decode_view<Codec, R, transcode_error_kind::expected>;
template<decode_codec Codec>
decode_or_error_closure decode_or_error_closure =
decode_closure<Codec, transcode_error_kind::expected>;
// [transcode.custom.encode], class template encode_view
template<encode_codec Codec, ranges::input_range R,
transcode_error_kind E = transcode_error_kind::replacement>
requires unicode_scalar_range<R>
class encode_view : public ranges::view_interface<encode_view<Codec, R, E>> {
R base; // exposition only
Codec codec; // exposition only
class iterator; // exposition only
public:
// [transcode.custom.encode], construction and access
constexpr explicit encode_view(R base, Codec codec = {});
constexpr const R& base() const& noexcept;
constexpr R base() &&;
constexpr iterator begin();
constexpr iterator begin() const
requires const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr iterator end()
requires ranges::forward_range<R> && ranges::common_range<R>;
constexpr iterator end() const
requires ranges::forward_range<const R> && ranges::common_range<const R> &&
const-iterator-compatible-range<R> && const-sentinel-compatible-range<R>;
constexpr default_sentinel_t end() const;
};
template<encode_codec Codec> constexpr encode_closure<Codec> encode(Codec codec = {});
template<encode_codec Codec>
constexpr encode_closure<Codec, transcode_error_kind::expected> encode_or_error(
Codec codec = {});
template<encode_codec Codec, ranges::input_range R>
encode_or_error_view encode_or_error_view =
encode_view<Codec, R, transcode_error_kind::expected>;
template<encode_codec Codec>
encode_or_error_closure encode_or_error_closure =
encode_closure<Codec, transcode_error_kind::expected>;
// [transcode.pipeline], transcoding pipelines
template<codec From, codec To> inline constexpr unspecified transcode;
template<decode_codec From, encode_codec To>
constexpr pluggable_transcode_closure<From, To> pluggable_transcode(From from, To to);
// [transcode.string], eager transcoding
constexpr bool has_whatwg_encoder(codec encoding) noexcept;
optional<string> transcode_string(span<const char> src, codec from, codec to);
optional<string> transcode_string(span<const char> src, string_view from_label,
string_view to_label);
// [transcode.bulk], eager bulk conversion
template<codec C, legacy_byte_range R> constexpr vector<char32_t> decode_to(R&& source);
template<codec C, typename Container = string, unicode_scalar_range R>
constexpr Container encode_to(R&& source);
template<codec C, legacy_byte_range R, output_iterator<char32_t> Output>
constexpr void decode_into(R&& source, Output output);
template<codec C, unicode_scalar_range R, output_iterator<char> Output>
constexpr void encode_into(R&& source, Output output);
template<decode_codec Codec, legacy_byte_range R>
constexpr vector<char32_t> decode_to(Codec codec, R&& source);
template<encode_codec Codec, typename Container = string, unicode_scalar_range R>
constexpr Container encode_to(Codec codec, R&& source);
template<decode_codec Codec, legacy_byte_range R, output_iterator<char32_t> Output>
constexpr void decode_into(Codec codec, R&& source, Output output);
template<encode_codec Codec, unicode_scalar_range R, output_iterator<char> Output>
constexpr void encode_into(Codec codec, R&& source, Output output);
// [transcode.codec.sniff], byte order mark sniffing
template<legacy_byte_range R>
requires ranges::forward_range<R>
constexpr optional<codec> sniff_encoding(R&& r) noexcept;
// [transcode.iconv], iconv adaptors
inline constexpr size_t iconv_min_buffer_size = 4;
struct iconv_functions {
iconv_t (*open)(const char* tocode, const char* fromcode);
size_t (*convert)(iconv_t cd, char** inbuf, size_t* inbytesleft, char** outbuf,
size_t* outbytesleft);
int (*close)(iconv_t cd);
};
template<typename IconvFns, ranges::input_range R>
requires legacy_byte_range<R>
class iconv_transcode_view
: public ranges::view_interface<iconv_transcode_view<IconvFns, R>> {
R base; // exposition only
IconvFns fns; // exposition only
const char* from; // exposition only
const char* to; // exposition only
span<char> buffer; // exposition only
public:
class iterator; // exposition only
// [transcode.iconv], construction and access
explicit iconv_transcode_view(R base, IconvFns fns, const char* from, const char* to,
span<char> buf);
const R& base() const& noexcept;
R base() &&;
iterator begin();
default_sentinel_t end() const;
};
template<typename IconvFns, ranges::input_range R>
requires legacy_byte_range<R>
class iconv_transcode_or_error_view
: public ranges::view_interface<iconv_transcode_or_error_view<IconvFns, R>> {
R base; // exposition only
IconvFns fns; // exposition only
const char* from; // exposition only
const char* to; // exposition only
span<char> buffer; // exposition only
public:
class iterator; // exposition only
// [transcode.iconv], construction and access
explicit iconv_transcode_or_error_view(R base, IconvFns fns, const char* from,
const char* to, span<char> buf);
const R& base() const& noexcept;
R base() &&;
iterator begin();
default_sentinel_t end() const;
};
inline iconv_functions make_real_iconv_fns() noexcept;
inline auto iconv_transcode(const char* from, const char* to, span<char> buf);
inline auto iconv_transcode_or_error(const char* from, const char* to, span<char> buf);
// [transcode.iconv], eager conversion
template<typename Container = string, typename IconvFns, legacy_byte_range R>
Container iconv_transcode_to(R&& source, const char* from, const char* to,
IconvFns fns);
template<typename Container = string, legacy_byte_range R>
Container iconv_transcode_to(R&& source, const char* from, const char* to);
template<typename IconvFns, legacy_byte_range R, output_iterator<char> Output>
Output iconv_transcode_into(R&& source, const char* from, const char* to, Output output,
IconvFns fns);
template<legacy_byte_range R, output_iterator<char> Output>
Output iconv_transcode_into(R&& source, const char* from, const char* to,
Output output);
template<typename Container = string, typename IconvFns, legacy_byte_range R>
expected<Container, iconv_error> iconv_transcode_to_or_error(R&& source,
const char* from,
const char* to,
IconvFns fns);
template<typename Container = string, legacy_byte_range R>
expected<Container, iconv_error> iconv_transcode_to_or_error(R&& source,
const char* from,
const char* to);1 Remarks: What a codec’s
decode_one returns.
is_error says whether the decode
failed. When it did, error says why
and code_point is U+FFFD REPLACEMENT
CHARACTER; when it did not,
code_point is the decoded Unicode
scalar value and error is
unspecified.
2 Remarks: What a codec’s
encode_one returns.
is_error says whether the encode
failed – the encoding has no representation for that scalar value. When
it did not, count is the number of
bytes written and
bytes[0]
through
bytes[count - 1]
are the encoded form; when it did,
count is zero and the view
substitutes or reports the error as its error kind says.
3 Remarks:
whatwg_decode_view<C, R, E>
presents the bytes of R as the
Unicode scalar values the encoding C
decodes them to, one element per decoded scalar value. A byte sequence
the encoding does not allow is a decoding error, reported as
E says: as U+FFFD REPLACEMENT
CHARACTER when E is
transcode_error_kind::replacement,
and as an unexpected holding a
whatwg_error when it is
transcode_error_kind::expected.
Decoding is lazy: an element is decoded when the iterator reaches it.
Every
char32_t
value the view presents is a Unicode scalar value.
4 The view models
random_access_range when
C names an encoding that decodes one
byte to one scalar value and R
models random_access_range. How an
implementation achieves that is not specified; it is a property of the
view rather than a second view.
5 Remarks:
whatwg_encode_view<C, R, E>
validates the
char32_t
elements of R as UTF-32 and presents
them as the bytes the encoding C
encodes them to, one element per byte. In replacement mode, an
ill-formed UTF-32 code unit is replaced with U+FFFD and that scalar
value is encoded normally; in expected mode it yields an
unexpected holding
whatwg_error::surrogate_code_point
or
whatwg_error::out_of_range.
A scalar value the encoding cannot represent yields
'?'
in replacement mode and
whatwg_error::unmapped_codepoint
in expected mode. Encoding is lazy, and one input element can produce
several output elements.
6 C
is required to be an encoding the WHATWG Encoding Standard defines an
encoder for. It defines none for
utf_16be,
utf_16le,
replacement or
x_user_defined, and the view does
not accept them.
constexpr iterator begin() const;7 Constraints:
R models
const-iterator-compatible-range.
constexpr iterator end() const;8 Constraints:
R models
const-iterator-compatible-range
and R models
range.
constexpr auto size() const;9 Constraints:
R models
sized_range.
10 Remarks:
decode_view<Codec, R, E>
is whatwg_decode_view
([transcode.whatwg.decode]) with the codec supplied as a value rather
than named by an enumerator: it presents the bytes of
R as the Unicode scalar values
Codec decodes them to, reports a
decoding error as E says, and
decodes lazily. Everything that clause says about the value type, the
error kind and the laziness holds here, of a codec the program wrote
rather than one the Encoding Standard defines. The view does not
validate a successful code_point;
the program-supplied Codec is
responsible for returning Unicode scalar values as required by
decode_codec.
11 The view models
random_access_range when
Codec models
random_access_decode_codec_type and
R models
random_access_range. When it does, a
byte whose value is
0x80 or
above and for which
Codec::decode_byte
returns U+FFFD is reported as
whatwg_error::invalid_byte
in expected mode and yields U+FFFD in replacement mode. This follows
from the U+FFFD reservation required by
random_access_decode_codec_type
([transcode.custom.reqs]).
static constexpr iterator terminal();12 Constraints:
R models
forward_range.
13 Remarks:
encode_view<Codec, R, E>
is whatwg_encode_view
([transcode.whatwg.encode]) with the codec supplied as a value rather
than named by an enumerator: it validates the
char32_t
elements of R as UTF-32, presents
the resulting scalar values as the bytes
Codec encodes them to, reports an
error as E says, and encodes lazily.
Ill-formed UTF-32 is replaced with U+FFFD before
Codec::encode_one
is called in replacement mode and is reported without calling the codec
in expected mode. A custom codec therefore receives only Unicode scalar
values.
static constexpr iterator terminal();14 Constraints:
R models
forward_range.
15 Remarks: The three POSIX
iconv entry points a view calls, as
a value the program supplies. open
opens a conversion descriptor,
convert converts, and
close closes it; each has the
signature and the semantics POSIX gives the function of the same name.
The views take this as a template parameter rather than calling
::iconv
directly so that a program can supply a different implementation of the
same interface – another library’s, or a test’s – and
make_real_iconv_fns is the one bound
to the platform’s.
16 The facilities in this subclause are
provided only by an implementation that supplies an
iconv conversion facility. The type
iconv_t in this subclause is
implementation-defined and denotes the handle type of that facility’s
conversion descriptors. On a POSIX implementation, it is the type POSIX
specifies as iconv_t.
17 Remarks:
iconv_transcode_view<IconvFns, R>
presents the bytes of R converted
from the encoding named by from to
the encoding named by to, one
element per output byte, using the
iconv implementation
IconvFns names. Conversion is lazy
and proceeds in batches: the view converts into the caller’s buffer,
yields those bytes, and converts again.
18 What the encoding names mean, which
pairs convert, and what a conversion does with input the source encoding
does not allow are the implementation’s
iconv’s, not this specification’s.
That is the point of the adaptor: it gives an interface a program
already has a ranges shape and a lifetime, and it does not restate a
table it does not own.
19 A conversion failure is reported as
iconv_error ([transcode.errors]),
which is the granularity POSIX reports at:
EILSEQ,
EINVAL and
E2BIG say that a byte
sequence is not valid, not why, so the WHATWG error vocabulary the other
views use would be claiming knowledge the OS does not return.
20 The output buffer is the caller’s,
and is not owned by the view. Its contents between two increments are
unspecified, and the program must keep it alive for the lifetime of
every iterator the view produces. If
iconv reports
E2BIG without producing a byte, the
range ends because its
char value
type has no error channel. A program that needs to detect that condition
uses iconv_transcode_or_error, which
reports
iconv_error::output_full
instead.
21 Remarks:
iconv_transcode_or_error_view<IconvFns, R>
is iconv_transcode_view
([transcode.iconv]) with the errors reported rather than skipped: its
value type is
expected<char, iconv_error>,
and a conversion failure is an element holding the
iconv_error POSIX reported rather
than input the range passes over. The buffer has no minimum-size
precondition. If it cannot hold one indivisible conversion or flush
unit, the view reports
iconv_error::output_full.
enum class whatwg_error {
invalid_byte,
truncated_sequence,
overlong_encoding,
surrogate_code_point,
out_of_range,
unmapped_codepoint,
};1 Remarks: An operation that fails to decode or to encode reports one of these values. The enumerators have the meanings in the following table.
Constant
|
Meaning
|
|---|---|
invalid_byte |
the input holds a byte the encoding does not allow in that position. |
truncated_sequence |
the input ends in the middle of a sequence. |
overlong_encoding |
the sequence encodes a value that a shorter sequence also encodes. |
surrogate_code_point |
a byte sequence decodes to, or a UTF-32 input code unit holds, a surrogate code point, which is not a Unicode scalar value. |
out_of_range |
a byte sequence decodes to, or a UTF-32 input code unit holds, a value greater than the largest Unicode scalar value. |
unmapped_codepoint |
the encoding has no representation for the Unicode scalar value being encoded. |
enum class iconv_error {
invalid_sequence,
incomplete_sequence,
output_full,
open_failed,
system_error,
};2 Remarks: An
iconv conversion that fails reports
one of these values, which include the three failures POSIX
iconv distinguishes and failures at
the boundary of that model. The enumerators have the meanings in the
following table.
Constant
|
Meaning
|
|---|---|
invalid_sequence |
the input is not valid in the source encoding, or has no
representation in the destination encoding
(EILSEQ). |
incomplete_sequence |
the input ends in the middle of a multibyte sequence
(EINVAL). |
output_full |
the conversion has no room left to write its result
(E2BIG). |
open_failed |
the conversion descriptor could not be opened;
iconv_open uses
EINVAL for an unsupported conversion
pair, so this condition is kept distinct from an incomplete input
sequence. |
system_error |
the conversion failed with an error POSIX does not specify for
iconv. |
enum class transcode_error_kind {
replacement,
expected,
};3 Remarks: A view’s error kind says how it reports a failure of the codec it drives. The enumerators have the meanings in the following table.
Constant
|
Meaning
|
|---|---|
replacement |
a failure to decode yields U+FFFD REPLACEMENT CHARACTER. Ill-formed
UTF-32 input is first replaced with U+FFFD and then encoded normally;
when an encoder cannot represent a scalar value, the encode view yields
'?'.
The view’s value type is the codec’s own. |
expected |
the view’s value type is
expected<T, whatwg_error>,
and a failure yields an unexpected
holding the error that occurred. |
template<typename R>
concept legacy_byte_range = ranges::range<R> && !is_array_v<remove_cvref_t<R>> &&
legacy-byte-type<remove_cv_t<ranges::range_value_t<R>>>;1 Remarks: An array type does
not satisfy legacy_byte_range, so
that the terminating null character of a string literal or of a
character array is not transcoded along with the rest of the array.
views::null_term
adapts a null-terminated character sequence, and
span a counted buffer.
template<typename R>
concept unicode_scalar_range =
ranges::input_range<R> && !is_array_v<remove_cvref_t<R>> &&
same_as<remove_cv_t<ranges::range_value_t<R>>, char32_t>;2 Remarks:
unicode_scalar_range constrains the
type of a range and not its values. Its
char32_t
elements are interpreted as UTF-32 code units by an encode operation: a
code unit that is not a Unicode scalar value is replaced with U+FFFD or
reported as an error, according to the operation’s error kind.
enum class codec {
utf_8,
replacement,
x_user_defined,
ibm866,
iso_8859_2,
iso_8859_3,
iso_8859_4,
iso_8859_5,
iso_8859_6,
iso_8859_7,
iso_8859_8,
iso_8859_8_i,
iso_8859_10,
iso_8859_13,
iso_8859_14,
iso_8859_15,
iso_8859_16,
koi8_r,
koi8_u,
macintosh,
windows_874,
windows_1250,
windows_1251,
windows_1252,
windows_1253,
windows_1254,
windows_1255,
windows_1256,
windows_1257,
windows_1258,
x_mac_cyrillic,
utf_16be,
utf_16le,
gbk,
gb18030,
big5,
shift_jis,
euc_jp,
iso_2022_jp,
euc_kr,
};1 Remarks: Each enumerator
names the encoding of the same name in the WHATWG Encoding Standard,
whose table of encodings is normative and is not restated here: an
encoding is what that standard says it is, and a specification that
copied the list would be a second one to keep current.
codec::replacement
is that standard’s replacement
encoding, which decodes any non-empty input to a single U+FFFD
REPLACEMENT CHARACTER, and
codec::x_user_defined
its
x-user-defined.
constexpr optional<codec> get_encoding(string_view label) noexcept;2 Returns: The
codec whose WHATWG label set
contains label, and
nullopt if no encoding has that
label. Leading and trailing ASCII whitespace is ignored and the
comparison is ASCII case-insensitive, which is the Encoding Standard’s
“get an encoding” algorithm.
3 Remarks: An encoding’s labels
are the alternative names a document may spell it with –
latin1,
iso-8859-1
and
windows-1252
are all labels of
codec::windows_1252
– so a lookup that fails is a name no encoding claims, not an encoding
that cannot be represented.
template<legacy_byte_range R>
requires ranges::forward_range<R>
constexpr optional<codec> sniff_encoding(R&& r) noexcept;4 Returns: The encoding
r begins with a byte order mark
for:
codec::utf_8,
if r begins with
EF BB BF;codec::utf_16be,
if r begins with
FE FF;codec::utf_16le,
if r begins with
FF FE;nullopt otherwise.5 Remarks: This is the byte order mark half of the Encoding Standard’s “decide the fallback encoding” step: what to do when there is no mark is a question about the document and its transport, which a library cannot answer. The mark itself is not consumed – a decode view strips a leading U+FEFF for the UTF codecs on its own – so the range can be passed on unchanged.
6 R
is required to be a forward_range
for that last sentence to be true. Examining the mark means advancing an
iterator over it, and on a single-pass range those bytes are not there
afterwards: sniffing a stream and then decoding it would drop the
prefix, and sniffing a stream with no mark – the common case – would
drop the first two bytes of the document. Nothing in the signature would
say so. A caller holding an
input_range has to materialize
enough of it to look twice.
constexpr const R& base() const& noexcept;1 Returns: Equivalent to:
return base;constexpr R base() &&;2 Returns: Equivalent to:
return move(base);constexpr explicit whatwg_decode_view(R base);3 Effects: Initializes
base with
std::move(base).
constexpr iterator begin();4 Returns: An
iterator over
base, positioned at its
first decoded element. Decoding the first element is part of forming the
iterator: a view over an input range cannot say whether it is empty
without reading it.
constexpr iterator begin() const;5 Constraints:
R models
const-iterator-compatible-range
and R models
const-sentinel-compatible-range.
6 Returns: An
iterator over
base, positioned at its
first decoded element.
constexpr iterator end();7 Constraints:
R models
forward_range and
R models
common_range.
8 Returns: An
iterator that compares
equal to an iterator that has decoded every element of
base.
constexpr iterator end() const;9 Constraints:
R models
forward_range,R models
common_range,R models
const-iterator-compatible-range,R models
const-sentinel-compatible-range.10 Returns: An
iterator that compares
equal to an iterator that has decoded every element of
base.
constexpr default_sentinel_t end() const;11 Returns:
default_sentinel.
template<codec C> inline constexpr unspecified whatwg_decode;12 Remarks: The name
whatwg_decode<C>
denotes a range adaptor object. Given a subexpression
E that models
legacy_byte_range, the expression
whatwg_decode<C>(E)
and
E | whatwg_decode<C>
are each expression-equivalent to a
whatwg_decode_view<C, views::all_t<decltype((E))>>
over E. An
E of array type is ill-formed: a
string literal carries a terminating null character that a decode would
render, and
views::null_term
([null.term.adaptor]) is how a null-terminated sequence becomes a
range.
template<codec C> inline constexpr unspecified whatwg_decode_or_error;13 Remarks:
whatwg_decode_or_error<C>
is
whatwg_decode<C>
with
transcode_error_kind::expected:
the view it adapts to has value type
expected<char32_t, whatwg_error>,
and a decoding error is the error rather than U+FFFD.
whatwg_decode_view::iterator
[transcode.whatwg.decode.iterator]constexpr iterator(base_iter current, base_sent end);14 Effects: Initializes
current with
std::move(current)
and end with
std::move(end),
then decodes the first element. When
C is a UTF encoding and that element
is U+FEFF BYTE ORDER MARK, it is discarded and the next element decoded
in its place: a byte order mark is how the encoding was chosen, not text
the document contains.
constexpr result_t operator*() const;15 Returns: Equivalent to:
return value;constexpr iterator& operator++();16 Effects: Decodes the next
element of the base range, or, if none remains, makes
*this
equal to
end().
17 Returns:
*this.
constexpr iterator operator++(int);18 Constraints:
R models
forward_range.
19 Effects: Equivalent to:
auto tmp = *this;
++*this;
return tmp;constexpr void operator++(int);20 Constraints:
ranges::forward_range<R>
is
false.
21 Effects: Equivalent to:
++*this;constexpr const R& base() const& noexcept;1 Returns: Equivalent to:
return base;constexpr R base() &&;2 Returns: Equivalent to:
return move(base);constexpr explicit whatwg_encode_view(R base);3 Effects: Initializes
base with
std::move(base).
constexpr iterator begin();4 Returns: An
iterator over
base, positioned at the
first byte of its first encoded element.
constexpr iterator begin() const;5 Constraints:
R models
const-iterator-compatible-range
and R models
const-sentinel-compatible-range.
6 Returns: An
iterator over
base, positioned at the
first byte of its first encoded element.
constexpr iterator end();7 Constraints:
R models
forward_range and
R models
common_range.
8 Returns: An
iterator that compares
equal to an iterator that has encoded every element of
base.
constexpr iterator end() const;9 Constraints:
R models
forward_range,R models
common_range,R models
const-iterator-compatible-range,R models
const-sentinel-compatible-range.10 Returns: An
iterator that compares
equal to an iterator that has encoded every element of
base.
constexpr default_sentinel_t end() const;11 Returns:
default_sentinel.
template<codec C> inline constexpr unspecified whatwg_encode;12 Remarks: The name
whatwg_encode<C>
denotes a range adaptor object. Given a subexpression
E that models
unicode_scalar_range, the expression
whatwg_encode<C>(E)
and
E | whatwg_encode<C>
are each expression-equivalent to a
whatwg_encode_view<C, views::all_t<decltype((E))>>
over E. An
E of array type is ill-formed.
template<codec C> inline constexpr unspecified whatwg_encode_or_error;13 Remarks:
whatwg_encode_or_error<C>
is
whatwg_encode<C>
with
transcode_error_kind::expected:
the view it adapts to has value type
expected<char, whatwg_error>,
and validation and encoding errors are reported rather than
replaced.
whatwg_encode_view::iterator
[transcode.whatwg.encode.iterator]constexpr iterator(base_iter current, base_sent end);14 Effects: Initializes
current with
std::move(current)
and end with
std::move(end),
then encodes the first element.
constexpr auto operator*() const;15 Returns: The byte at the current position of the encoded element, or the error the encoding of that element produced.
constexpr iterator& operator++();16 Effects: Advances to the
next byte of the encoded element, encoding the next element of the base
range when the current one is exhausted; if no element remains, makes
*this
equal to
end(). When
C is a stateful encoding, exhausting
the base range also emits whatever the encoding owes to return to its
initial state.
17 Returns:
*this.
constexpr iterator operator++(int);18 Constraints:
R models
forward_range.
19 Effects: Equivalent to:
auto tmp = *this;
++*this;
return tmp;constexpr void operator++(int);20 Constraints:
ranges::forward_range<R>
is
false.
21 Effects: Equivalent to:
++*this;template<typename C>
concept decode_codec = semiregular<C> && requires(C& c, const unsigned char*& iter,
const unsigned char* sentinel) {
{ c.decode_one(iter, sentinel) } -> same_as<decode_result>;
};1 Remarks: A type models
decode_codec if it can decode one
Unicode scalar value from a byte sequence.
c.decode_one(iter, sentinel)
reads from iter up to but not past
sentinel, advances
iter by the bytes it consumed, and
returns what it decoded or the error that stopped it. It is called only
with
iter != sentinel.
A codec that consumes no byte does not terminate the view that drives
it.
2 A codec is a value:
semiregular because a view owns one
and copies it, and a codec that decodes a sequence with state keeps that
state in itself, not in the range.
template<typename C>
concept random_access_decode_codec_type =
decode_codec<C> && requires(const C& c, unsigned char byte) {
{ c.decode_byte(byte) } -> same_as<char32_t>;
};3 Remarks: A type models
random_access_decode_codec_type if
each byte decodes to one scalar value independently of every other:
c.decode_byte(byte)
returns what byte alone decodes to,
and U+FFFD if it decodes to nothing. For byte values
0x80 and
above, U+FFFD is reserved as that “decodes to nothing” signal; the
author of such a codec cannot map one of those bytes to U+FFFD itself. A
view over such a codec is a
random_access_range, since the
nth element is the nth byte.
template<typename C>
concept encode_codec = semiregular<C> && requires(C& c, char32_t cp) {
{ c.encode_one(cp) } -> same_as<encode_result>;
};4 Remarks: A type models
encode_codec if it can encode one
Unicode scalar value to bytes.
c.encode_one(cp)
returns the encoded form of cp, or
an error if the encoding has no representation for it. An encode view
validates its UTF-32 input before calling this function, so
cp is always a Unicode scalar
value.
template<typename C>
concept flushable_decode_codec = decode_codec<C> && requires(C& c) {
{ c.flush() } -> same_as<optional<decode_result>>;
};5 Remarks: A type models
flushable_decode_codec if it can owe
output after the last byte.
c.flush()
is called once, when the input is exhausted, and returns what the codec
still owes – a truncation error for a sequence that ended mid-way, say –
or nullopt if it owes nothing. A
codec that does not model this concept is asked for nothing at the end
of the input.
decode_view
[transcode.custom.decode]constexpr const R& base() const& noexcept;1 Returns: Equivalent to:
return base;constexpr R base() &&;2 Returns: Equivalent to:
return move(base);constexpr explicit decode_view(R base, Codec codec = {});3 Effects: Initializes
base with
std::move(base)
and codec with
codec.
constexpr iterator begin();4 Returns: An
iterator over
base and a copy of
codec, positioned at its
first decoded element.
constexpr iterator begin() const;5 Constraints:
R models
const-iterator-compatible-range
and R models
const-sentinel-compatible-range.
6 Returns: An
iterator over
base and a copy of
codec, positioned at its
first decoded element.
constexpr iterator end();7 Constraints:
R models
forward_range and
R models
common_range.
8 Returns: An
iterator that compares
equal to an iterator that has decoded every element of
base.
constexpr iterator end() const;9 Constraints:
R models
forward_range,R models
common_range,R models
const-iterator-compatible-range,R models
const-sentinel-compatible-range.10 Returns: An
iterator that compares
equal to an iterator that has decoded every element of
base.
constexpr default_sentinel_t end() const;11 Returns:
default_sentinel.
template<decode_codec Codec> constexpr see below decode(Codec codec = {});12 Returns: A range adaptor
object over codec. Given a
subexpression E that models
legacy_byte_range,
decode(codec)(E)
and
E | decode(codec)
are each expression-equivalent to a
decode_view<Codec, views::all_t<decltype((E))>>
over E and a copy of
codec. An
E of array type is ill-formed.
template<decode_codec Codec> constexpr see below decode_or_error(Codec codec = {});13 Returns:
decode(codec)
with
transcode_error_kind::expected:
the view it adapts to has value type
expected<char32_t, whatwg_error>,
and a decoding error is the error rather than U+FFFD.
decode_view::iterator
[transcode.custom.decode.iterator]constexpr iterator(base_iter current, base_sent end, Codec codec);14 Effects: Initializes
current with
std::move(current),
end with
std::move(end)
and the iterator’s own copy of the codec with
codec, then decodes the first
element.
constexpr result_t operator*() const;15 Returns: Equivalent to:
return value;constexpr iterator& operator++();16 Effects: Decodes the next
element of the base range, or, if none remains, makes
*this
equal to
end(). When
Codec models
flushable_decode_codec, exhausting
the base range calls
flush() once
and yields what it returns, if anything.
17 Returns:
*this.
constexpr iterator operator++(int);18 Constraints:
R models
forward_range.
19 Effects: Equivalent to:
auto tmp = *this;
++*this;
return tmp;constexpr void operator++(int);20 Constraints:
ranges::forward_range<R>
is
false.
21 Effects: Equivalent to:
++*this;encode_view
[transcode.custom.encode]constexpr const R& base() const& noexcept;1 Returns: Equivalent to:
return base;constexpr R base() &&;2 Returns: Equivalent to:
return move(base);constexpr explicit encode_view(R base, Codec codec = {});3 Effects: Initializes
base with
std::move(base)
and codec with
codec.
constexpr iterator begin();4 Returns: An
iterator over
base and a copy of
codec, positioned at the
first byte of its first encoded element.
constexpr iterator begin() const;5 Constraints:
R models
const-iterator-compatible-range
and R models
const-sentinel-compatible-range.
6 Returns: An
iterator over
base and a copy of
codec, positioned at the
first byte of its first encoded element.
constexpr iterator end();7 Constraints:
R models
forward_range and
R models
common_range.
8 Returns: An
iterator that compares
equal to an iterator that has encoded every element of
base.
constexpr iterator end() const;9 Constraints:
R models
forward_range,R models
common_range,R models
const-iterator-compatible-range,R models
const-sentinel-compatible-range.10 Returns: An
iterator that compares
equal to an iterator that has encoded every element of
base.
constexpr default_sentinel_t end() const;11 Returns:
default_sentinel.
template<encode_codec Codec> constexpr see below encode(Codec codec = {});12 Returns: A range adaptor
object over codec. Given a
subexpression E that models
unicode_scalar_range,
encode(codec)(E)
and
E | encode(codec)
are each expression-equivalent to an
encode_view<Codec, views::all_t<decltype((E))>>
over E and a copy of
codec.
template<encode_codec Codec> constexpr see below encode_or_error(Codec codec = {});13 Returns:
encode(codec)
with
transcode_error_kind::expected:
the view it adapts to has value type
expected<char, whatwg_error>,
and an encoding or UTF-32 validation error is reported rather than
replaced.
encode_view::iterator
[transcode.custom.encode.iterator]constexpr iterator(base_iter current, base_sent end, Codec codec);14 Effects: Initializes
current with
std::move(current),
end with
std::move(end)
and the iterator’s own copy of the codec with
codec, then encodes the first
element.
constexpr result_t operator*() const;15 Returns: The byte at the current position of the encoded element, or the error the encoding of that element produced.
constexpr iterator& operator++();16 Effects: Advances to the
next byte of the encoded element, encoding the next element of the base
range when the current one is exhausted; if no element remains, makes
*this
equal to
end().
17 Returns:
*this.
constexpr iterator operator++(int);18 Constraints:
R models
forward_range.
constexpr void operator++(int);19 Constraints:
ranges::forward_range<R>
is
false.
template<codec From, codec To> inline constexpr unspecified transcode;1 Remarks: The name
transcode<From, To>
denotes a range adaptor object. Given a subexpression
E that models
legacy_byte_range,
transcode<From, To>(E)
and
E | transcode<From, To>
are each expression-equivalent to
E | whatwg_decode<From> | whatwg_encode<To>.
An E of array type is
ill-formed.
2 The composition is the specification:
the pipeline decodes and re-encodes one element at a time, and it is
lazy because its parts are. A scalar value
To cannot represent is an encoding
error, reported as whatwg_encode
reports one; a byte sequence From
does not allow is a decoding error, and the U+FFFD it decodes to is what
gets encoded.
template<decode_codec From, encode_codec To>
constexpr see below pluggable_transcode(From from, To to);3 Returns: A range adaptor
object over from and
to. Given a subexpression
E that models
legacy_byte_range,
pluggable_transcode(from, to)(E)
and
E | pluggable_transcode(from, to)
are each expression-equivalent to
E | decode(from) | encode(to).
4 Remarks: This is
transcode ([transcode.pipeline])
with codecs supplied as values rather than named by enumerators.
constexpr bool has_whatwg_encoder(codec encoding) noexcept;1 Returns:
true if the
WHATWG Encoding Standard defines an encoder for
encoding, and
false
otherwise.
optional<string> transcode_string(span<const char> src, codec from, codec to);2 Returns:
nullopt if the WHATWG Encoding
Standard defines no encoder for to;
otherwise, the bytes of src, decoded
as from and re-encoded as
to. Errors are substituted rather
than reported: a byte sequence from
does not allow decodes to U+FFFD, and a scalar value
to cannot represent encodes to
'?'.
3 Remarks: This is
transcode ([transcode.pipeline]) run
to completion into a string, which
is what a caller who wants the whole result and not a view writes.
src | transcode<From, To> | ranges::to<string>()
is the same thing with the codecs known at compile time.
optional<string> transcode_string(span<const char> src, string_view from_label,
string_view to_label);4 Returns:
transcode_string(src, *get_encoding(from_label), *get_encoding(to_label)),
and nullopt if either label names no
encoding ([transcode.codec.label]) or the target encoding has no WHATWG
encoder.
5 Remarks: This is the overload
a program uses when the encodings are a runtime choice – a
Content-Type
header, a command-line option – which is what labels are for.
template<codec C, legacy_byte_range R> constexpr vector<char32_t> decode_to(R&& source);1 Returns: Equivalent to:
return forward<R>(source) | whatwg_decode<C> | ranges::to<vector<char32_t>>();2 Remarks: Each of these is the
pipeline it names, run to completion. They add no behaviour to it: the
errors are the decoding view’s, substituted rather than reported
([transcode.whatwg.decode]), and a caller who wants them reported writes
the pipeline with
whatwg_decode_or_error instead. What
they add is the name, for the case a caller wants the whole result
rather than a view over it.
template<codec C, typename Container = string, unicode_scalar_range R>
constexpr Container encode_to(R&& source);3 Returns: Equivalent to:
return forward<R>(source) | whatwg_encode<C> | ranges::to<Container>();4 Remarks:
Container is the caller’s choice of
result, defaulted to string because
that is what encoded bytes are usually wanted in.
template<codec C, legacy_byte_range R, output_iterator<char32_t> Output>
constexpr void decode_into(R&& source, Output output);5 Effects: Equivalent to:
ranges::copy(forward<R>(source) | whatwg_decode<C>, output);template<codec C, unicode_scalar_range R, output_iterator<char> Output>
constexpr void encode_into(R&& source, Output output);6 Effects: Equivalent to:
ranges::copy(forward<R>(source) | whatwg_encode<C>, output);template<decode_codec Codec, legacy_byte_range R>
constexpr vector<char32_t> decode_to(Codec codec, R&& source);7 Returns: Equivalent to:
return forward<R>(source) | decode(codec) | ranges::to<vector<char32_t>>();8 Remarks: The same four
operations over a codec supplied as a value ([transcode.custom.reqs]),
for a codec chosen at run time or one the
codec enumeration does not name.
template<encode_codec Codec, typename Container = string, unicode_scalar_range R>
constexpr Container encode_to(Codec codec, R&& source);9 Returns: Equivalent to:
return forward<R>(source) | encode(codec) | ranges::to<Container>();template<decode_codec Codec, legacy_byte_range R, output_iterator<char32_t> Output>
constexpr void decode_into(Codec codec, R&& source, Output output);10 Effects: Equivalent to:
ranges::copy(forward<R>(source) | decode(codec), output);template<encode_codec Codec, unicode_scalar_range R, output_iterator<char> Output>
constexpr void encode_into(Codec codec, R&& source, Output output);11 Effects: Equivalent to:
ranges::copy(forward<R>(source) | encode(codec), output);const R& base() const& noexcept;1 Returns: Equivalent to:
return base;R base() &&;2 Returns: Equivalent to:
return move(base);const R& base() const& noexcept;3 Returns: Equivalent to:
return base;R base() &&;4 Returns: Equivalent to:
return move(base);inline constexpr size_t iconv_min_buffer_size = 4;5 Remarks: The smallest output
buffer accepted by
iconv_transcode_view. An
iconv implementation can still
report E2BIG without producing a
byte when a destination encoding needs a larger indivisible output
unit.
explicit iconv_transcode_view(R base, IconvFns fns, const char* from, const char* to,
span<char> buf);6 Preconditions:
buf.size() >= iconv_min_buffer_size
is true.
7 Effects: Initializes the view
with
std::move(base),
std::move(fns),
from,
to and
buf. No conversion descriptor is
opened: begin opens one.
iterator begin();8 Returns: An
iterator over
base holding a conversion
descriptor opened by
fns.open(to, from).
9 Remarks: Each call opens a
descriptor, so a view converts once per
begin. The iterator owns the
descriptor and closes it, which is what makes the adaptor leak-free
where the POSIX interface is not.
default_sentinel_t end() const;10 Returns:
default_sentinel.
explicit iconv_transcode_or_error_view(R base, IconvFns fns, const char* from,
const char* to, span<char> buf);11 Effects: Initializes the
view with
std::move(base),
std::move(fns),
from,
to and
buf.
iterator begin();12 Returns: An
iterator over
base holding a conversion
descriptor opened by
fns.open(to, from).
default_sentinel_t end() const;13 Returns:
default_sentinel.
inline iconv_functions make_real_iconv_fns() noexcept;14 Returns: An
iconv_functions whose members are
the implementation’s iconv_open,
iconv and
iconv_close.
15 Remarks: The facilities in
this subclause are provided only by an implementation that supplies an
iconv conversion facility.
inline see below iconv_transcode(const char* from, const char* to, span<char> buf);16 Preconditions:
buf.size() >= iconv_min_buffer_size
is true.
17 Returns: A range adaptor
object. Given a subexpression E that
models legacy_byte_range,
iconv_transcode(from, to, buf)(E)
and
E | iconv_transcode(from, to, buf)
are each expression-equivalent to an
iconv_transcode_view
([transcode.iconv]) over E,
make_real_iconv_fns(),
from,
to and
buf.
18 Remarks:
buf is the caller’s, and every
iterator the adaptor produces converts into it. Two ranges adapted with
the same buffer must not be iterated at the same time.
inline see below iconv_transcode_or_error(const char* from, const char* to,
span<char> buf);19 Returns:
iconv_transcode(from, to, buf)
with the errors reported rather than skipped: the view it adapts to has
value type
expected<char, iconv_error>.
20 Remarks: Unlike
iconv_transcode, this adaptor
accepts a buffer of any size. If
iconv reports
E2BIG without producing output, the
adapted view reports
iconv_error::output_full.
template<typename Container = string, typename IconvFns, legacy_byte_range R>
Container iconv_transcode_to(R&& source, const char* from, const char* to,
IconvFns fns);21 Returns: A
Container holding the bytes of
source converted from
from to
to by
fns. Input the conversion does not
accept is skipped, as it is by
iconv_transcode_view
([transcode.iconv]). The result is empty when the conversion descriptor
cannot be opened – which is what
iconv_open failing means, and is not
distinguishable here from an empty input. An unexpected system failure
ends conversion and returns the bytes produced before the failure. Use
iconv_transcode_to_or_error when
either distinction matters.
template<typename Container = string, legacy_byte_range R>
Container iconv_transcode_to(R&& source, const char* from, const char* to);22 Returns: Equivalent to:
return iconv_transcode_to<Container>(forward<R>(source), from, to,
make_real_iconv_fns());template<typename IconvFns, legacy_byte_range R, output_iterator<char> Output>
Output iconv_transcode_into(R&& source, const char* from, const char* to, Output output,
IconvFns fns);23 Effects: Converts the bytes
of source from
from to
to by
fns and writes them through
output.
24 Returns: The value of
output after the last byte
written.
25 Remarks: If the conversion
descriptor cannot be opened, no bytes are written and the unchanged
iterator is returned. This is not distinguishable from empty input; use
iconv_transcode_to_or_error when the
distinction matters. An unexpected system failure similarly returns the
iterator after the last byte successfully written.
template<legacy_byte_range R, output_iterator<char> Output>
Output iconv_transcode_into(R&& source, const char* from, const char* to,
Output output);26 Returns: Equivalent to:
return iconv_transcode_into(forward<R>(source), from, to, move(output),
make_real_iconv_fns());template<typename Container = string, typename IconvFns, legacy_byte_range R>
expected<Container, iconv_error> iconv_transcode_to_or_error(R&& source,
const char* from,
const char* to,
IconvFns fns);27 Returns: A
Container holding the converted
bytes, or the first iconv_error the
conversion reported.
28 Remarks: This is the eager
form of iconv_transcode_or_error
([transcode.iconv]): it stops at the first failure rather than skipping
it, which is the difference between a conversion a program wants to know
about and one it wants to get through.
template<typename Container = string, legacy_byte_range R>
expected<Container, iconv_error> iconv_transcode_to_or_error(R&& source,
const char* from,
const char* to);29 Returns: Equivalent to:
return iconv_transcode_to_or_error<Container>(forward<R>(source), from, to,
make_real_iconv_fns());iconv_transcode_view::iterator
and
iconv_transcode_or_error_view::iterator
[transcode.iconv.iterator]iterator(iterator&&) noexcept;30 Effects: Takes over
other’s conversion descriptor and
leaves other holding none.
31 Remarks: The iterator is move-only. A conversion descriptor is an OS-owned handle that cannot be duplicated, so copying one would either close it twice or leak it.
iterator& operator=(iterator&&) noexcept;32 Effects: Closes the
descriptor
*this
holds, if any, then takes over
other’s and leaves
other holding none.
33 Returns:
*this.
~iterator();34 Effects: Closes the
conversion descriptor, if
*this
holds one.
char operator*() const;35 Returns: The converted byte at the current position of the output buffer.
iterator& operator++();36 Effects: Advances to the
next converted byte, converting more input when the buffer is exhausted;
if no input and no unconverted bytes remain, makes
*this
equal to default_sentinel.
37 Returns:
*this.
38 Remarks: Input the
conversion does not accept is skipped one byte at a time – what POSIX
reports as EILSEQ – so the range
ends where the input does rather than at the first byte a converter
refuses. A program that needs to know that it happened uses
iconv_transcode_or_error
([transcode.iconv]).
void operator++(int);39 Effects: Equivalent to:
++*this;iterator(iterator&&) noexcept;40 Effects: Takes over
other’s conversion descriptor and
leaves other holding none.
iterator& operator=(iterator&&) noexcept;41 Effects: Closes the
descriptor
*this
holds, if any, then takes over
other’s and leaves
other holding none.
42 Returns:
*this.
~iterator();43 Effects: Closes the
conversion descriptor, if
*this
holds one.
expected<char, iconv_error> operator*() const;44 Returns: The converted byte
at the current position of the output buffer, or the
iconv_error the conversion reported
there.
iterator& operator++();45 Effects: Advances to the
next element, converting more input when the buffer is exhausted; if no
input and no unconverted bytes remain, makes
*this
equal to default_sentinel.
46 Returns:
*this.
void operator++(int);47 Effects: Equivalent to:
++*this;<null_term>
synopsis [null.term.syn]struct null_sentinel_t {
// [null.term.sentinel], comparison
template<input_iterator I>
requires requires(I i) {
{ *i == 0 };
}
friend constexpr bool operator==(const I& it, null_sentinel_t);
};
inline constexpr null_sentinel_t null_sentinel{};
template<contiguous_iterator I>
class null_term_view : public ranges::view_interface<null_term_view<I>> {
I ptr{}; // exposition only
public:
// [null.term.view], construction and access
constexpr null_term_view() = default;
constexpr explicit null_term_view(I ptr);
constexpr I begin() const;
constexpr null_sentinel_t end() const;
};
// [null.term.adaptor], range adaptor object
inline constexpr unspecified null_term;null_sentinel_t
[null.term.sentinel]template<input_iterator I>
requires requires(I i) {
{ *i == 0 };
}
friend constexpr bool operator==(const I& it, null_sentinel_t);1 Returns:
*it == 0.
null_term_view [null.term.view]constexpr null_term_view() = default;1 Remarks: The resulting view
is singular. It may be assigned to and destroyed, but iterating it
results in undefined behavior because the iterator returned by
begin() has
no reachable terminator.
constexpr explicit null_term_view(I ptr);2 Preconditions: There is an
iterator i reachable from
ptr by a finite sequence of
applications of
++ such that
*i == 0,
and every element from ptr through
i, inclusive, is within the lifetime
of a single object.
3 Effects: Initializes
ptr with
ptr.
constexpr I begin() const;4 Returns: Equivalent to:
return ptr;constexpr null_sentinel_t end() const;5 Returns:
null_sentinel_t().
views::null_term
[null.term.adaptor]inline constexpr unspecified null_term;1 Remarks: The name
views::null_term
denotes a range adaptor object. Given a subexpression
E, the expression
views::null_term(E)
is expression-equivalent to
null_term_view(E)
when E has pointer type, and to
null_term_view(static_cast<const T*>(E))
when E has array type with element
type T. For any other type it is
ill-formed, so that a range carrying no terminator is a diagnosed error
and not a silent one. The precondition of
null_term_view’s converting
constructor applies to E after any
array-to-pointer conversion.
Boost.Text
and extensive work on Unicode in C++.This design did not arise in isolation. It draws on several earlier libraries and proposals that explored encoding-aware iteration, transcoding APIs, and implementation techniques.
Tom Honermann’s text_view library
and paper [P0244R2],[text_view]
established an early range-based model for enumerating encoded text as
code points. It made the encoding explicit in the type system, and it
ran into the ergonomics problem that follows from doing so: iteration
has to surface decoding status somehow.
Zach Laine’s
Boost.Text
[Boost.Text]
integrates range-based text facilities with C++ algorithms and
pipelines, and its Unicode-oriented design and normalization support
mark out territory that a proposal about external byte streams should
stay out of.
JeanHeyd Meneide’s P1629R1
proposal and
ztd.text
implementation [P1629R1],[ztd.text] go the other
way: a broad text and transcoding library, eager and lazy interfaces
both, encoding objects as values, extensible throughout. The surface
here is narrower and fixed on browser-compatible WHATWG transcoding. But
the split between lazy views and eager bulk helpers is the same
admission that both usage styles are needed.
Henri Sivonen’s encoding_rs [encoding_rs]
is the important implementation prior art for WHATWG-compatible
transcoding specifically. It targets the Encoding Standard directly,
provides streaming and bulk-oriented APIs both, and validates against
the web-platform tests while still chasing fast paths. Exact WHATWG
semantics, WPT-derived conformance coverage, and eager helpers alongside
the range adaptors are all lessons taken from it.
The deliberate departure from
text_view, Boost.Text, and
ztd.text is
scope. Their general encoding abstractions can represent more models,
but that breadth makes the common boundary operation harder to name and
specify. This proposal fixes one compatibility model, WHATWG, and gives
it direct decode, encode, and collection names. The narrower surface is
the point, not an intermediate step toward another general text
framework.
[P2728R13] proposes
transcoding views for UTF-8, UTF-16, and UTF-32 that operate on the
strict Unicode character types
(char8_t,
char16_t,
char32_t).
This proposal overlaps in the UTF portions but differs in scope and
philosophy:
Aspect
|
P2728
|
This Proposal
|
|---|---|---|
| Input types | char8_t,
char16_t,
char32_t |
char,
signed char,
unsigned char,
std::byte |
| UTF-16 model | char16_t
code units (native order) |
Byte stream with explicit BE/LE |
| Encodings | UTF-8, UTF-16, UTF-32 only | 40 codec identifiers, including WHATWG special and decode-only entries |
| Focus | Strict Unicode processing | Web/legacy interoperability |
| Use case | Internal Unicode manipulation | External data ingestion |
The UTF-16 distinction is particularly important. P2728 operates on
char16_t,
which represents UTF-16 code units already in the platform’s native byte
order. This is appropriate for in-memory Unicode processing where the
data originated from C++ code.
This proposal instead decodes UTF-16 from raw byte streams:
// P2728: char16_t input (native byte order, already code units)
std::u16string s = u"Hello";
auto codepoints = s | std::uc::to_utf32;
// This proposal: byte input with explicit byte order
std::vector<std::byte> network_data = read_socket();
auto codepoints = network_data | whatwg_decode<codec::utf_16be>;When UTF-16 data arrives from external sources — network protocols,
file formats, binary blobs — it arrives as bytes with a specific byte
order, determined by the protocol or the BOM. The host’s own endianness
does not enter into it. The
codec::utf_16be
and
codec::utf_16le
variants decode these byte streams correctly either way.
The proposals are complementary:
P2728 is ideal when working with well-typed Unicode data within an application. Its strict typing catches encoding mismatches at compile time.
This proposal is necessary when ingesting data
from external sources — network protocols, file formats, databases —
where bytes arrive as
char*
or
std::byte*.
The encoding is determined by metadata or heuristics that the C++ type
system never sees.
A typical workflow might use this proposal to decode legacy-encoded
input to
char32_t,
process it, then use P2728’s facilities for UTF-to-UTF conversions
within the application, and finally use this proposal again to encode
output for a legacy system.
The UTF-8 decode/encode in this proposal follows WHATWG semantics (replacement character on error), which may differ slightly from P2728’s error handling. Applications requiring strict UTF validation should prefer P2728 for that portion of the pipeline.
The three implementation families in this proposal have matching API surfaces for every operation their encoding model supports. The P2728 column shows the parallel design in the proposed standard UTF transcoding views.
API
|
WHATWG
|
Pluggable codec
|
iconv
|
P2728
|
|---|---|---|---|---|
| Codec identity | codec::utf_8
enum |
my_codec{}
type |
"UTF-8"
string |
char8_t/char16_t/char32_t |
| Decode view | whatwg_decode<C> |
decode(codec) |
iconv_transcode(f,t,buf) |
views::to_utf32 |
| Decode or-error view | whatwg_decode_or_error<C> |
decode_or_error(codec) |
iconv_transcode_or_error(…) |
views::to_utf32_or_error |
| Encode view | whatwg_encode<C> |
encode(codec) |
n/a ¹ | views::to_utf8
/ to_utf16 |
| Encode or-error view | whatwg_encode_or_error<C> |
encode_or_error(codec) |
n/a ¹ | views::to_utf8_or_error |
| Transcode pipeline | transcode<From,To> |
pluggable_transcode(f,t) |
iconv_transcode(f,t,buf) |
compose via
| |
| Bulk decode → container | decode_to<C>(r) |
decode_to(codec,r) |
n/a ¹ | v\|ranges::to<>() |
| Bulk encode → container | encode_to<C,Cont>(r) |
encode_to(codec,r) |
n/a ¹ | v\|ranges::to<>() |
| Bulk transcode → container | n/a ² | n/a ² | iconv_transcode_to(range,f,t) |
n/a ² |
| Bulk decode → output iter | decode_into<C>(r,out) |
decode_into(codec,r,out) |
n/a ¹ | ranges::copy(v, out) |
| Bulk encode → output iter | encode_into<C>(r,out) |
encode_into(codec,r,out) |
n/a ¹ | ranges::copy(v, out) |
| Bulk transcode → output iter | n/a ² | n/a ² | iconv_transcode_into(range,f,t,out) |
n/a ² |
| Null-terminated input | views::null_term(ptr) |
views::null_term(ptr) |
views::null_term(ptr) |
n/a ³ |
| Runtime label lookup | get_encoding("utf-8") |
n/a ⁴ | n/a (string labels are the API) | n/a ⁵ |
| Runtime transcode | optional<string> transcode_string(src,from,to) |
n/a ⁴ | iconv_transcode_to(r,f,t) |
n/a ⁵ |
| BOM sniffing | sniff_encoding(range) |
n/a ⁶ | n/a ⁶ | n/a ⁷ |
| Error type | whatwg_error |
whatwg_error |
iconv_error |
utf_transcoding_error |
| Output element type | char32_t |
char32_t |
char
(raw bytes) |
char32_t |
| Input element type | char/byte
(legacy) |
char/byte
(legacy) |
char
(any byte encoding) |
char8_t/char16_t/char32_t |
| constexpr | yes | yes | no (POSIX userland library) | yes |
Notes on n/a entries:
¹ iconv is a byte↔︎byte transcoder. It converts one
byte encoding directly to another without exposing an intermediate
char32_t
stage. Encode (from
char32_t to
bytes) and decode (from bytes to
char32_t) as
separate steps are outside its model.
² WHATWG and pluggable codecs compose decode and
encode. The named bulk helpers forward to
view | ranges::to<Container>()
and
ranges::copy(view, output),
and a bulk transcode is
decode | encode
composed with
| and
collected the same way, so there is no separate compile-time-selected
helper for it. transcode_string is
the eager transcoder, and it is in the Runtime transcode row
below rather than this one because its codecs are values and not
template parameters, which is the reason it exists. iconv performs
single-pass byte→byte conversion and exposes it as a first-class bulk
operation.
³ P2728 operates on typed Unicode character types
(char8_t,
char16_t,
char32_t);
views::null_term
produces a range of
char.
Bridging the two requires a reinterpret step that is outside both
proposals.
⁴ Pluggable codecs are identified by C++ type. Codec
selection happens at compile time through the type system; there is no
runtime name-to-codec registry by design. Runtime transcode is similarly
outside the model: you compose
decode(codec_a{}) | encode(codec_b{})
at compile time.
⁵ P2728 selects its codec from the character type.
Selection is determined by the character types
(char8_t,
char16_t,
char32_t),
so runtime label lookup and runtime transcode are outside its model.
⁶ BOM sniffing is a property of the byte stream.
sniff_encoding()
examines the first bytes of a stream to detect UTF-8, UTF-16BE or
UTF-16LE BOMs and returns the appropriate
codec enum value. This is a
WHATWG-specific facility; pluggable codecs and iconv operate on
already-identified encodings.
⁷ P2728 operates on in-memory typed data where the encoding is known from the type. BOM detection is an I/O concern; a separate endian-converting view proposal addresses byte-order issues.
The shared design patterns across all four columns are the
_or_error suffix for error-surfacing
variants,
char32_t as
the interchange type, and composition with
|.
beman.transcode;
it has not been published under the Beman project’s organization yet, so
that is where the code is.