Transcoding Text Views

Range adapters for character encoding conversion

Document #: D4246R1 [Latest] [Status]
Date: 2026-09-22
Project: Programming Language C++
Audience: SG16
SG9
LEWGI
LEWG
Reply-to: Steve Downey
<>

1 Abstract

“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.

2 Changes since R0

3 Context and Motivation

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.

4 Comparison Table

4.1 Decoding UTF-8 to Unicode code points

Before: manual loop
After: whatwg_decode
std::vector<char32_t> decode_utf8(
    std::string_view input) {
  std::vector<char32_t> result;
  size_t i = 0;
  while (i < input.size()) {
    unsigned char b = input[i];
    char32_t cp; int extra;
    if (b < 0x80) { cp = b; extra = 0; }
    else if ((b & 0xE0) == 0xC0)
      { cp = b & 0x1F; extra = 1; }
    else if ((b & 0xF0) == 0xE0)
      { cp = b & 0x0F; extra = 2; }
    else if ((b & 0xF8) == 0xF0)
      { cp = b & 0x07; extra = 3; }
    else { result.push_back(U'\xFFFD');
           ++i; continue; }
    if (i + extra >= input.size()) {
      result.push_back(U'\xFFFD'); break;
    }
    for (int j = 0; j < extra; ++j) {
      unsigned char c = input[++i];
      if ((c & 0xC0) != 0x80) {
        cp = U'\xFFFD'; break;
      }
      cp = (cp << 6) | (c & 0x3F);
    }
    // Missing: overlong, surrogate checks
    result.push_back(cp);
    ++i;
  }
  return result;
}
std::vector<char32_t> decode_utf8(
    std::string_view input) {
  return input
    | whatwg_decode<codec::utf_8>
    | std::ranges::to<std::vector>();
}

4.2 Transcoding Shift_JIS to UTF-8

Before: iconv
After: decode then encode
std::string shift_jis_to_utf8(
    std::string_view input) {
  iconv_t cd = iconv_open("UTF-8",
                          "SHIFT_JIS");
  if (cd == (iconv_t)-1)
    throw std::runtime_error("iconv_open");

  std::string result;
  result.resize(input.size() * 4);

  char* inbuf = const_cast<char*>(
                  input.data());
  size_t inleft = input.size();
  char* outbuf = result.data();
  size_t outleft = result.size();

  while (inleft > 0) {
    size_t r = iconv(cd, &inbuf, &inleft,
                     &outbuf, &outleft);
    if (r == (size_t)-1) {
      if (errno == E2BIG) {
        size_t used = outbuf - result.data();
        result.resize(result.size() * 2);
        outbuf = result.data() + used;
        outleft = result.size() - used;
      } else if (errno == EILSEQ) {
        // Skip invalid byte
        ++inbuf; --inleft;
        // Append replacement char
        *outbuf++ = '\xEF';
        *outbuf++ = '\xBF';
        *outbuf++ = '\xBD';
        outleft -= 3;
      } else {
        iconv_close(cd);
        throw std::runtime_error("iconv");
      }
    }
  }
  result.resize(outbuf - result.data());
  iconv_close(cd);
  return result;
}
std::string shift_jis_to_utf8(
    std::string_view input) {
  return input
    | whatwg_decode<codec::shift_jis>
    | whatwg_encode<codec::utf_8>
    | std::ranges::to<std::string>();
}

4.3 Converting a null-terminated C string

Before: mbstowcs
After: views::null_term
std::wstring from_cstring(const char* s) {
  // Assumes locale is set correctly
  std::setlocale(LC_ALL, "");
  size_t len = std::mbstowcs(nullptr, s, 0);
  if (len == (size_t)-1)
    throw std::runtime_error("mbstowcs");
  std::wstring result(len, L'\0');
  std::mbstowcs(result.data(), s, len + 1);
  return result;
  // Problems:
  // - Global locale state
  // - wchar_t is not portable
  // - No error recovery
  // - Two passes required
}
auto from_cstring(const char* s) {
  return views::null_term(s)
    | whatwg_decode<codec::utf_8>;
  // Returns lazy view of char32_t
  // No global state
  // Portable Unicode scalars
  // Single pass, errors yield U+FFFD
}

4.4 Processing with error visibility

Before: manual tracking
After: _or_error
// No standard way to detect errors
// during transcoding. Either:
// 1. Errors are silently replaced
// 2. Exceptions thrown mid-stream
// 3. Custom state machine required

bool has_errors = false;
std::vector<char32_t> result;
// ... complex manual decoding with
// error tracking interspersed ...
if (has_errors) {
  log_warning("Invalid UTF-8 detected");
}
for (auto r : input
    | whatwg_decode_or_error<codec::utf_8>) {
  if (r.has_value()) {
    process(*r);
  } else {
    log_warning(r.error());
    process(U'\xFFFD');
  }
}

4.5 Bulk conversion to owned storage

Before: manual collection loop
After: decode_to
std::vector<char32_t> decode_utf8(
    std::string_view input) {
  std::vector<char32_t> result;
  for (char32_t cp : input
      | whatwg_decode<codec::utf_8>) {
    result.push_back(cp);
  }
  return result;
}
std::vector<char32_t> decode_utf8(
    std::string_view input) {
  return decode_to<codec::utf_8>(input);
}

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)

5 Design Rationale

5.1 The Status Quo

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.

5.2 Why WHATWG?

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.

5.3 Why Forward-Only Iterators?

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.

6 Design

6.1 Overview

The proposal adds four user-facing adaptor families:

Four eager bulk helpers, each specified as the pipeline it names:

And three utility entry points:

6.2 Codec Enumeration

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.

6.3 Label Lookup for Runtime Codec Selection

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:

A 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
}

6.4 Error Handling

Two WHATWG error handling strategies are provided:

  1. 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.

  2. 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
};

6.4.1 Relationship to 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.

6.5 Concepts

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.

6.6 Constexpr Support

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'é');

6.7 Pipe Syntax

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>;

6.8 Range Properties and Const Support

All of the views are valid input_ranges, and the WHATWG views preserve stronger range properties where they can.

6.9 User-Facing Examples

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>();

7 Design Decisions

7.1 Byte-Like Types, Not Character Types

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:

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-8

7.2 Template Parameter vs Runtime Selection

Making 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.

7.3 char32_t as the Interchange Type

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.

7.4 Separation of Decode and Encode

Rather than a single primary “transcode from A to B” adaptor, decode and encode are separate operations composed via pipes. This enables:

The 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.

7.5 Leading BOM Stripping for UTF Decoders

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.

7.6 No Normalization

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.

8 Methods

8.1 Authoritative Source Acquisition

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.

8.2 Python Preprocessing for Tables and Conformance Vectors

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.

9 Testing

9.1 Web Platform Tests

The implementation is validated against the WHATWG Web Platform Tests (WPT) [WPT-Encoding] for text encoding. These tests provide:

Test 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.

9.2 Negative Compile Tests

Every concept constraint has a corresponding negative compile test — a .cpp file that must fail to compile with a specific diagnostic. This ensures:

9.3 Consteval Tests

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'€');

9.4 Roundtrip Tests

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.

10 Impact on the Standard

This proposal adds new headers and does not modify existing standard library components. It is a pure extension.

10.1 Feature Test Macro

#define __cpp_lib_transcode_view 202XXXL

10.2 Headers

#include <transcode>   // whatwg_decode, whatwg_encode, transcode, iconv_transcode, transcode_string
#include <null_term>   // null_term_view

Or combined into <ranges> additions.

11 Open Question for SG16

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.

12 Summary of the Design

The decisions embodied in the design, and in the wording that follows:

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.

13 Wording

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.

13.1 Editing instructions

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]

Header <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.

Error types [transcode.errors]

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.

Enum class whatwg_error
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.

Enum class iconv_error
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.

Enum class transcode_error_kind
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.

Range requirements [transcode.reqs]

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.

Encodings [transcode.codec]

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.

Label lookup [transcode.codec.label]

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.

Byte order mark sniffing [transcode.codec.sniff]

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:

  • (4.1) codec::utf_8, if r begins with EF BB BF;
  • (4.2) codec::utf_16be, if r begins with FE FF;
  • (4.3) codec::utf_16le, if r begins with FF FE;
  • (4.4) 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.

Decoding views [transcode.whatwg.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 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:

  • (9.1) R models forward_range,
  • (9.2) R models common_range,
  • (9.3) R models const-iterator-compatible-range,
  • (9.4) 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.

Class 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;

Encoding views [transcode.whatwg.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 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:

  • (9.1) R models forward_range,
  • (9.2) R models common_range,
  • (9.3) R models const-iterator-compatible-range,
  • (9.4) 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.

Class 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;

Codec requirements [transcode.custom.reqs]

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.

Class template 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:

  • (9.1) R models forward_range,
  • (9.2) R models common_range,
  • (9.3) R models const-iterator-compatible-range,
  • (9.4) 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.

Class 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;

Class template 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:

  • (9.1) R models forward_range,
  • (9.2) R models common_range,
  • (9.3) R models const-iterator-compatible-range,
  • (9.4) 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.

Class 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.

Transcoding pipelines [transcode.pipeline]

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.

Eager transcoding [transcode.string]

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.

Eager bulk conversion [transcode.bulk]

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);

iconv adaptors [transcode.iconv]

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());

Classes 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;

Header <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;

Class 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.

Class template 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.

14 Acknowledgements

15 Prior Art

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.

16 Relationship to Other Proposals

16.1 P2728 Unicode in the Library

[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:

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.

16.2 API Surface Comparison

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 |.

17 References

[Boost.Text] Zach Laine. 2018. Boost.Text.
https://github.com/tzlaine/text
[encoding_rs] Henri Sivonen. 2018. encoding_rs.
https://github.com/hsivonen/encoding_rs
[P0244R2] Tom Honermann. 2017-06-13. Text_view: A C++ concepts and range based character encoding and code point enumeration library.
https://wg21.link/p0244r2
[P1629R1] JeanHeyd Meneide. 2020-03-02. Transcoding the world - Standard Text Encoding.
https://wg21.link/p1629r1
[P2728R13] Eddie Nolan. 2026-05-27. Unicode in the Library, Part 1: UTF Transcoding.
https://wg21.link/p2728r13
[P2871R3] Alisdair Meredith. 2023-12-18. Remove Deprecated Unicode Conversion Facets From C++26.
https://wg21.link/p2871r3
[P2873R3] Alisdair Meredith, Tom Honermann. 2025-03-17. Remove Deprecated locale category facets for Unicode from C++26.
https://wg21.link/p2873r3
[Schultke] Jan Schultke. 2026. Personal correspondence.
[text_view] Tom Honermann. 2017. text_view.
https://github.com/tahonermann/text_view
[WHATWG-Encoding] WHATWG. 2026-05-21. Encoding Standard (Living Standard, commit a985b62a9b45c17da3e17a9f0a0b4e30c34c4a8a, last updated 21 May 2026).
https://encoding.spec.whatwg.org/commit-snapshots/a985b62a9b45c17da3e17a9f0a0b4e30c34c4a8a/
[WPT-Encoding] W3C Web Platform Tests Contributors. 2026. Web Platform Tests — Encoding.
https://github.com/web-platform-tests/wpt/tree/master/encoding
[ztd.text] JeanHeyd Meneide. 2021. ztd.text.
https://github.com/soasis/text