Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions include/beman/utf_view/code_unit_view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import beman.utf_view;
#include <beman/utf_view/detail/concepts.hpp>
#if !BEMAN_UTF_VIEW_USE_MODULES()
#include <beman/transform_view/transform_view.hpp>
#include <concepts>
#include <cstddef>
#include <ranges>
#include <type_traits>
#endif
Expand All @@ -33,9 +35,9 @@ namespace detail {
/* PAPER */

template <class T>
struct exposition_only_implicit_cast_to {
struct exposition_only_cast_to {
constexpr T operator()(auto x) const noexcept {
return x;
return static_cast<T>(x);
}
};

Expand All @@ -45,19 +47,33 @@ struct exposition_only_implicit_cast_to {

namespace detail {

// A source type U may be cast to the code unit type Char if it is Char
// itself, or if it is std::byte or a non-bool integral type of the same
// width as Char. This rules out narrowing, widening (e.g. accidentally
// treating UTF-8 code units as code points), floating-point sources, bool,
// enumerations other than std::byte, and class types such as proxy
// references.
template <class U, class Char>
concept code_unit_castable =
std::same_as<U, Char> ||
((std::same_as<U, std::byte> ||
(std::is_integral_v<U> && !std::same_as<U, bool>)) &&
sizeof(U) == sizeof(Char));

template <typename Char>
struct as_code_unit_impl
: std::ranges::range_adaptor_closure<as_code_unit_impl<Char>> {
template <std::ranges::range R>
requires std::convertible_to<std::ranges::range_reference_t<R>, Char> &&
requires code_unit_castable<
std::remove_cvref_t<std::ranges::range_reference_t<R>>, Char> &&
(!std::is_array_v<std::remove_cvref_t<R>>)
constexpr auto operator()(R&& r) const {
using T = std::remove_cvref_t<R>;
if constexpr (detail::is_empty_view<T>) {
return std::ranges::empty_view<Char>{};
} else {
return beman::transform_view::transform_view(
std::forward<R>(r), exposition_only_implicit_cast_to<Char>{});
std::forward<R>(r), exposition_only_cast_to<Char>{});
}
}
};
Expand Down
70 changes: 62 additions & 8 deletions papers/P2728.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ for casting between character types: `as_char`, `as_wchar_t`, `as_char8_t`,
`as_char16_t`, and `as_char32_t`.

These are syntactic sugar for producing a `std::ranges::transform_view` with
an exposition-only transformation functor that performs the needed cast.
an exposition-only transformation functor that performs the needed cast. The
rule governing which source types may be cast is discussed in
[Code Unit Adaptor Conversion Rule](#code-unit-adaptor-conversion-rule).

# Additional Examples

Expand Down Expand Up @@ -1143,24 +1145,27 @@ Add the following subclause to [range.adaptors]{.sref}:

```cpp
template<class T>
struct @*implicit-cast-to*@ { // @*exposition only*@/
constexpr T operator()(auto x) const noexcept { return x; }
struct @*cast-to*@ { // @*exposition only*@
constexpr T operator()(auto x) const noexcept { return static_cast<T>(x); }
};
```

The names `as_char`, `as_wchar_t`, `as_char8_t`, `as_char16_t`, and
`as_char32_t` denote range adaptor objects ([range.adaptor.object]). Let
`as_charT` denote any one of `as_char`, `as_wchar_t`, `as_char8_t`,
`as_char16_t`, and `as_char32_t`. Let `Char` be the corresponding character
type for `as_charT`, let `E` be an expression and let `T` be
`remove_cvref_t<decltype((E))>`. If `ranges::range_reference_t<T>` does not
model `convertible_to<Char>`, or if `T` is an array, `as_charT(E)` is
ill-formed. Otherwise, the expression `as_charT(E)` is expression-equivalent to:
type for `as_charT`, let `E` be an expression, let `T` be
`remove_cvref_t<decltype((E))>`, and let `U` be
`remove_cvref_t<ranges::range_reference_t<T>>`. `as_charT(E)` is ill-formed if
`T` is an array, or if `U` is not `Char` and `U` is not a type `X` such that
`sizeof(X) == sizeof(Char)` and either `X` is `byte` or `X` is an integral type
other than `bool`. Otherwise, the expression `as_charT(E)` is
expression-equivalent to:

- If `T` is a specialization of `empty_view` ([range.empty.view]), then
`empty_view<Char>{}`.

- Otherwise, `ranges::transform_view(std::views::all(E), @*implicit-cast-to*@<Char>{})`.
- Otherwise, `ranges::transform_view(std::views::all(E), @*cast-to*@<Char>{})`.

\[Example 1:
```c++
Expand Down Expand Up @@ -1365,6 +1370,53 @@ But we voted in favor of providing it for forward ranges:
>
> A: I think it's probably useful, but I'm not certain, so let's wait.

## Code Unit Adaptor Conversion Rule

For the `as_charT` casting views, the rule we use to determine whether the element type
`U` of a view's source range makes it eligible for conversion to the target type `Char`
is:

- `U` is `Char`, or
- `U` is `std::byte` or a non-`bool` integral type, and `sizeof(U) == sizeof(Char)`.

This accomplishes the following design goals:

- Rules out casts which are either illogical (floats, bools) or which are likely to be
mistakes (class types, enums other than `std::byte`)
- Rules out narrowing and widening conversions by the width requirement, including between
`charN_t` types
- Allows conversions from same-width integral types and `std::byte` to `charN_t` types,
such that `int` can be used to store `char32_t` (as is done in ICU) and `unsigned short`
can be used for `char16_t` (assuming those types are appropriately sized), and `wchar_t`
can either be cast to `char16_t` or `char32_t` depending on its width.

This is consistent with [@P3695R3]'s deprecation of most implicit conversions between
`charN_t` types. Despite the fact that that paper only drops implicit casts, and the
`as_charT` views can be likened to an explicit cast, I think it's appropriate here to only
bless casts that we think usually make sense, and make the user work a bit harder to spell
out a `transform_view` if, for example, they want to cast between `char16_t` and
`char32_t` (whose implicit conversions [@P3695R3] preserved, but which I don't think
merits an explicit carveout here since there's no justification based on backwards
compatibility).

| Source | `as_char` | `as_char8_t` | `as_char16_t` | `as_char32_t` | `as_wchar_t` |
|---|---|---|---|---|---|
| `char` | yes | yes | no | no | no |
| `signed char`, `unsigned char` | yes | yes | no | no | no |
| `std::byte` | yes | yes | no | no | no |
| `char8_t` | yes | yes | no | no | no |
| `char16_t` | no | no | yes | no | same width |
| `char32_t` | no | no | no | yes | same width |
| `wchar_t` | no | no | same width | same width | yes |
| `short`, `unsigned short` | no | no | yes | no | same width |
| `int`, `unsigned int` | no | no | no | same width | same width |
| `long`, `unsigned long` | no | no | no | same width | same width |
| `long long`, `unsigned long long` | no | no | no | no | no |
| `bool` | no | no | no | no | no |
| floating-point types | no | no | no | no | no |
| enumerations other than `std::byte` | no | no | no | no | no |
| class types | no | no | no | no | no |

# Changelog

## Changes since R14
Expand All @@ -1374,6 +1426,8 @@ But we voted in favor of providing it for forward ranges:
- Implement `it.base_code_units()` for forward ranges per SG9 feedback
- Add random access support for UTF-32-to-UTF-32 self-transcode per SG9
feedback
- Constrain the code unit adaptors to same-width integral and `std::byte`
sources per SG16 feedback, and add a design discussion of the rule

## Changes since R13

Expand Down
142 changes: 142 additions & 0 deletions tests/beman/utf_view/code_unit_view.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
#if BEMAN_UTF_VIEW_USE_MODULES()
import std;
#else
#include <array>
#include <cstddef>
#include <cstdint>
#include <ranges>
#include <span>
#include <string>
#include <string_view>
#include <vector>
#endif

namespace beman::utf_view::tests {
Expand Down Expand Up @@ -101,6 +106,87 @@ static_assert(
test_random_access_iterator<char>, test_random_access_iterator<char>>>()
| as_char8_t)>>);

// Conversion rule: a source type is accepted if it is the target type, or if it
// is std::byte or a non-bool integral type of the same width as the target.
template <class R, auto const& Adaptor>
constexpr bool adaptable = requires { std::declval<R>() | Adaptor; };

enum unscoped_byte_enum : unsigned char { unscoped_byte_enum_value };
enum class scoped_byte_enum : unsigned char { value };
struct convertible_to_char8_t {
constexpr operator char8_t() const { return u8'x'; }
};

// as_char8_t
static_assert(adaptable<std::string_view, as_char8_t>);
static_assert(adaptable<std::u8string_view, as_char8_t>);
static_assert(adaptable<std::span<signed char const>, as_char8_t>);
static_assert(adaptable<std::span<unsigned char const>, as_char8_t>);
static_assert(adaptable<std::span<std::byte const>, as_char8_t>);
static_assert(adaptable<std::span<std::uint8_t const>, as_char8_t>);
static_assert(adaptable<std::span<std::int8_t const>, as_char8_t>);
static_assert(!adaptable<std::u16string_view, as_char8_t>);
static_assert(!adaptable<std::u32string_view, as_char8_t>);
static_assert(!adaptable<std::wstring_view, as_char8_t>);
static_assert(!adaptable<std::span<short const>, as_char8_t>);
static_assert(!adaptable<std::span<int const>, as_char8_t>);
static_assert(!adaptable<std::span<bool const>, as_char8_t>);
static_assert(!adaptable<std::vector<bool>&, as_char8_t>);
static_assert(!adaptable<std::span<float const>, as_char8_t>);
static_assert(!adaptable<std::span<double const>, as_char8_t>);
static_assert(!adaptable<std::span<unscoped_byte_enum const>, as_char8_t>);
static_assert(!adaptable<std::span<scoped_byte_enum const>, as_char8_t>);
static_assert(!adaptable<std::span<convertible_to_char8_t const>, as_char8_t>);
static_assert(!adaptable<char const (&)[4], as_char8_t>);

// as_char
static_assert(adaptable<std::string_view, as_char>);
static_assert(adaptable<std::u8string_view, as_char>);
static_assert(adaptable<std::span<unsigned char const>, as_char>);
static_assert(adaptable<std::span<std::byte const>, as_char>);
static_assert(!adaptable<std::u16string_view, as_char>);
static_assert(!adaptable<std::u32string_view, as_char>);

// as_char16_t
static_assert(adaptable<std::u16string_view, as_char16_t>);
static_assert(adaptable<std::span<short const>, as_char16_t>);
static_assert(adaptable<std::span<unsigned short const>, as_char16_t>);
static_assert(adaptable<std::span<std::uint16_t const>, as_char16_t>);
static_assert(adaptable<std::wstring_view, as_char16_t> ==
(sizeof(wchar_t) == sizeof(char16_t)));
static_assert(!adaptable<std::u8string_view, as_char16_t>);
static_assert(!adaptable<std::u32string_view, as_char16_t>);
static_assert(!adaptable<std::string_view, as_char16_t>);
static_assert(!adaptable<std::span<std::byte const>, as_char16_t>);

// as_char32_t
static_assert(adaptable<std::u32string_view, as_char32_t>);
static_assert(adaptable<std::span<std::uint32_t const>, as_char32_t>);
static_assert(adaptable<std::span<std::int32_t const>, as_char32_t>);
static_assert(adaptable<std::span<int const>, as_char32_t> ==
(sizeof(int) == sizeof(char32_t)));
static_assert(adaptable<std::span<unsigned const>, as_char32_t> ==
(sizeof(unsigned) == sizeof(char32_t)));
static_assert(adaptable<std::span<long const>, as_char32_t> ==
(sizeof(long) == sizeof(char32_t)));
static_assert(adaptable<std::wstring_view, as_char32_t> ==
(sizeof(wchar_t) == sizeof(char32_t)));
static_assert(!adaptable<std::u8string_view, as_char32_t>);
static_assert(!adaptable<std::u16string_view, as_char32_t>);
static_assert(!adaptable<std::span<long long const>, as_char32_t>);
static_assert(!adaptable<std::span<double const>, as_char32_t>);

// as_wchar_t
static_assert(adaptable<std::wstring_view, as_wchar_t>);
static_assert(adaptable<std::u16string_view, as_wchar_t> ==
(sizeof(wchar_t) == sizeof(char16_t)));
static_assert(adaptable<std::u32string_view, as_wchar_t> ==
(sizeof(wchar_t) == sizeof(char32_t)));
static_assert(adaptable<std::span<unsigned short const>, as_wchar_t> ==
(sizeof(wchar_t) == sizeof(unsigned short)));
static_assert(!adaptable<std::u8string_view, as_wchar_t>);
static_assert(!adaptable<std::string_view, as_wchar_t>);

constexpr bool smoke_test() {
std::string_view foo{"foo"};
auto bar{foo | as_char8_t};
Expand All @@ -120,6 +206,50 @@ constexpr bool special_case_test() {
return true;
}

constexpr bool same_type_test() {
std::u8string_view foo{u8"foo"};
auto bar{foo | as_char8_t};
if ((bar | std::ranges::to<std::u8string>()) != foo) {
return false;
}
return true;
}

constexpr bool byte_test() {
std::array<std::byte, 3> const euro{std::byte{0xE2}, std::byte{0x82},
std::byte{0xAC}};
auto const as_utf8{euro | as_char8_t | std::ranges::to<std::u8string>()};
if (as_utf8 != std::u8string_view{u8"\u20AC"}) {
return false;
}
return true;
}

// The cast is as if by static_cast, so negative values in a signed source
// preserve their bit pattern.
constexpr bool signed_source_test() {
std::array<signed char, 3> const euro{static_cast<signed char>(-30),
static_cast<signed char>(-126),
static_cast<signed char>(-84)};
auto const as_utf8{euro | as_char8_t | std::ranges::to<std::u8string>()};
if (as_utf8 != std::u8string_view{u8"\u20AC"}) {
return false;
}
return true;
}

constexpr bool int_source_test() {
if constexpr (sizeof(int) == sizeof(char32_t)) {
std::array<int, 2> const code_points{0x1F574, 0xFFFD};
auto const as_utf32{code_points | as_char32_t |
std::ranges::to<std::u32string>()};
if (as_utf32 != std::u32string_view{U"\U0001F574\uFFFD"}) {
return false;
}
}
return true;
}

constexpr bool value_category_test() {
std::string foo{"foo"};
(void)(foo | as_char8_t);
Expand All @@ -136,6 +266,18 @@ CONSTEXPR_UNLESS_MSVC bool code_unit_view_test() {
if (!special_case_test()) {
return false;
}
if (!same_type_test()) {
return false;
}
if (!byte_test()) {
return false;
}
if (!signed_source_test()) {
return false;
}
if (!int_source_test()) {
return false;
}
if (!value_category_test()) {
return false;
}
Expand Down
Loading