Skip to content

Commit 0898f41

Browse files
committed
Restrict unsafe_narrow, add unsafe_cast
Closes #1191 Restrict `unsafe_narrow` to narrowing cases and arithmetic types Make `as` diagnose unsafe pointer casts, with a message to use `unsafe_cast` instead Allow both `unsafe_narrow` and `unsafe_cast` to be used without `cpp2::` qualification in Cpp2 code Add a new "unsafe casts" doc section Remove some uses of `unsafe_narrow` in cppfront's own code that weren't actually narrowing Example: f: (i: i32, inout s: std::string) = { // j := i as i16; // error, maybe-lossy narrowing j := unsafe_narrow<i16>(i); // ok, 'unsafe' is explicit pv: *void = s&; // pi := pv as *std::string; // error, unsafe cast ps := unsafe_cast<*std::string>(pv); // ok, 'unsafe' is explicit ps* = "plugh"; } main: () = { str: std::string = "xyzzy"; f( 42, str ); std::cout << str; // prints: plush }
1 parent f5363cc commit 0898f41

File tree

16 files changed

+168
-36
lines changed

16 files changed

+168
-36
lines changed

docs/cpp2/expressions.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ _ = vec.emplace_back(1,2,3);
9999
For details, see [Design note: Explicit discard](https://github.com/hsutter/cppfront/wiki/Design-note%3A-Explicit-discard). In Cpp2, data is always initialized, data is never silently lost, data flow is always visible. Data is precious, and it's always safe.
100100

101101

102-
## <a id="is"></a> `is` — safe type/value queries
102+
## Type/value queries and casts
103+
104+
### <a id="is"></a> `is` — safe type/value queries
103105

104106
An `x is C` expression allows safe type and value queries, and evaluates to `#!cpp true` if `x` matches constraint `C`. It supports both static and dynamic queries, including customization, with support for standard library dynamic types like `std::variant`, `std::optional`, `std::expected`, and `std::any` provided out of the box.
105107

@@ -147,7 +149,7 @@ Here are some `is` queries with their Cpp1 equivalents. In this table, uppercase
147149
> Note: `is` unifies a variety of differently-named Cpp1 language and library queries under one syntax, and supports only the type-safe ones.
148150
149151

150-
## <a id="as"></a> `as` — safe casts and conversions
152+
### <a id="as"></a> `as` — safe casts and conversions
151153

152154
An `x as T` expression allows safe type casts. `x` must be an object or expression, and `T` must be a type. Like `is`, `as` supports both static and dynamic typing, including customization, with support for standard library dynamic types like `std::variant`, `std::optional`, `std::expected`, and `std::any` provided out of the box. For example:
153155

@@ -184,6 +186,24 @@ Here are some `as` casts with their Cpp1 equivalents. In this table, uppercase n
184186
> Note: `as` unifies a variety of differently-named Cpp1 language and library casts and conversions under one syntax, and supports only the type-safe ones.
185187
186188

189+
### <a id="unsafe-casts"></a> Unsafe casts
190+
191+
Unsafe casts must always be explicit.
192+
193+
To perform a numeric narrowing cast, such as `i32` to `i16` or `u32`, use `unsafe_narrow<To>(from)`. For example:
194+
195+
``` cpp title="Unsafe narrowing and casts must be explicit" hl_lines="2 3 6 7"
196+
f: (i: i32, inout s: std::string) = {
197+
// j := i as i16; // error, maybe-lossy narrowing
198+
j := unsafe_narrow<i16>(i); // ok, 'unsafe' is explicit
199+
200+
pv: *void = s&;
201+
// pi := pv as *std::string; // error, unsafe cast
202+
pi := unsafe_cast<*std::string>(pv); // ok, 'unsafe' is explicit
203+
}
204+
```
205+
206+
187207
## <a id="inspect"></a> `inspect` — pattern matching
188208

189209
An `inspect expr -> Type = { /* alternatives */ }` expression allows pattern matching using `is`.
@@ -353,3 +373,4 @@ std::cout << "now x+2 is (x+2)$\n";
353373
```
354374

355375
A string literal capture can include a `:suffix` where the suffix is a [standard C++ format specification](https://en.cppreference.com/w/cpp/utility/format/spec). For example, `#!cpp (x.price(): <10.2f)$` evaluates `x.price()` and converts the result to a string with 10-character width, 2 digits of precision, and left-justified.
376+

include/cpp2regex.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3111,7 +3111,7 @@ size_t i{0};
31113111
auto number {0};
31123112
if (!(string_util::string_to_int(group, number, 8))) {return ctx.error("Could not convert octal to int."); }
31133113

3114-
char number_as_char {unsafe_narrow<char>(cpp2::move(number))};
3114+
char number_as_char {cpp2::unsafe_narrow<char>(cpp2::move(number))};
31153115

31163116
auto token {CPP2_UFCS_TEMPLATE(cpp2_new<char_token>)(cpp2::shared, number_as_char, ctx.get_modifiers().has(expression_flags::case_insensitive))};
31173117
(*cpp2::impl::assert_not_null(token)).set_string("\\" + cpp2::to_string(string_util::int_to_string<8>(cpp2::impl::as_<int>(cpp2::move(number_as_char)))) + "");
@@ -3462,7 +3462,7 @@ template<typename CharT, int group, bool case_insensitive> [[nodiscard]] auto gr
34623462
if (!(string_util::string_to_int(cpp2::move(number_str), number, 16))) {return ctx.error("Could not convert hexadecimal to int."); }
34633463

34643464
// TODO: Change for unicode.
3465-
char number_as_char {unsafe_narrow<char>(cpp2::move(number))};
3465+
char number_as_char {cpp2::unsafe_narrow<char>(cpp2::move(number))};
34663466

34673467
std::string syntax {string_util::int_to_string<16>(cpp2::impl::as_<int>(number_as_char))};
34683468
if (cpp2::move(has_brackets)) {
@@ -3605,7 +3605,7 @@ template<typename CharT, bool positive> [[nodiscard]] auto lookahead_token_match
36053605
if (!(string_util::string_to_int(cpp2::move(number_str), number, 8))) {return ctx.error("Could not convert octal to int."); }
36063606

36073607
// TODO: Change for unicode.
3608-
char number_as_char {unsafe_narrow<char>(cpp2::move(number))};
3608+
char number_as_char {cpp2::unsafe_narrow<char>(cpp2::move(number))};
36093609

36103610
std::string syntax {"\\o{" + cpp2::to_string(string_util::int_to_string<8>(cpp2::impl::as_<int>(number_as_char))) + "}"};
36113611
auto r {CPP2_UFCS_TEMPLATE(cpp2_new<char_token>)(cpp2::shared, cpp2::move(number_as_char), ctx.get_modifiers().has(expression_flags::case_insensitive))};
@@ -3981,7 +3981,7 @@ template<typename CharT, bool negate> [[nodiscard]] auto word_boundary_token_mat
39813981
template <typename CharT, typename matcher_wrapper> template <typename Iter> regular_expression<CharT,matcher_wrapper>::search_return<Iter>::search_return(cpp2::impl::in<bool> matched_, context<Iter> const& ctx_, Iter const& pos_)
39823982
: matched{ matched_ }
39833983
, ctx{ ctx_ }
3984-
, pos{ unsafe_narrow<int>(std::distance(ctx_.begin, pos_)) }{
3984+
, pos{ cpp2::unsafe_narrow<int>(std::distance(ctx_.begin, pos_)) }{
39853985

39863986
#line 2633 "cpp2regex.h2"
39873987
}

include/cpp2util.h

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,7 @@ inline constexpr auto is( X const& x, bool (*value)(X const&) ) -> bool {
16811681

16821682
// The 'as' cast functions are <To, From> so use that order here
16831683
// If it's confusing, we can switch this to <From, To>
1684+
16841685
template< typename To, typename From >
16851686
inline constexpr auto is_narrowing_v =
16861687
// [dcl.init.list] 7.1
@@ -1694,7 +1695,20 @@ inline constexpr auto is_narrowing_v =
16941695
(std::is_integral_v<From> && std::is_integral_v<To> && sizeof(From) > sizeof(To)) ||
16951696
(std::is_enum_v<From> && std::is_integral_v<To> && sizeof(From) > sizeof(To)) ||
16961697
// [dcl.init.list] 7.5
1697-
(std::is_pointer_v<From> && std::is_same_v<To, bool>);
1698+
(std::is_pointer_v<From> && std::is_same_v<To, bool>)
1699+
;
1700+
1701+
template< typename To, typename From >
1702+
inline constexpr auto is_unsafe_pointer_conversion_v =
1703+
std::is_pointer_v<To>
1704+
&& std::is_pointer_v<From>
1705+
// Work around Clang <= 15 C++20 mode not conforming to C++20 P0929
1706+
#if (defined(__clang_major__) && __clang_major__ <= 15)
1707+
&& !std::is_same_v<std::remove_cvref_t<To>, void*>
1708+
#else
1709+
&& !requires (To t, From f) { t = f; }
1710+
#endif
1711+
;
16981712

16991713
template <typename... Ts>
17001714
inline constexpr auto program_violates_type_safety_guarantee = sizeof...(Ts) < 0;
@@ -1807,6 +1821,12 @@ auto as(auto&& x CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT_AS) -> decltype(auto)
18071821
{
18081822
return Dynamic_cast<C>(CPP2_FORWARD(x));
18091823
}
1824+
else if constexpr (
1825+
is_unsafe_pointer_conversion_v<C, CPP2_TYPEOF(x)>
1826+
)
1827+
{
1828+
return nonesuch;
1829+
}
18101830
else if constexpr (requires { C{CPP2_FORWARD(x)}; }) {
18111831
// Experiment: Recognize the nested `::value_type` pattern for some dynamic library types
18121832
// like std::optional, and try to prevent accidental narrowing conversions even when
@@ -2242,7 +2262,23 @@ class finally_presuccess
22422262
//-----------------------------------------------------------------------
22432263
//
22442264
template <typename C, typename X>
2245-
constexpr auto unsafe_narrow( X&& x ) noexcept -> decltype(auto)
2265+
constexpr auto unsafe_narrow( X x ) noexcept
2266+
-> decltype(auto)
2267+
requires (
2268+
impl::is_narrowing_v<C, X>
2269+
|| (
2270+
std::is_arithmetic_v<C>
2271+
&& std::is_arithmetic_v<X>
2272+
)
2273+
)
2274+
{
2275+
return static_cast<C>(x);
2276+
}
2277+
2278+
2279+
template <typename C, typename X>
2280+
constexpr auto unsafe_cast( X&& x ) noexcept
2281+
-> decltype(auto)
22462282
{
22472283
return static_cast<C>(CPP2_FORWARD(x));
22482284
}
@@ -2291,16 +2327,16 @@ struct args
22912327
int curr;
22922328
};
22932329

2294-
auto begin() const -> iterator { return iterator{ argc, argv, 0 }; }
2295-
auto end() const -> iterator { return iterator{ argc, argv, argc }; }
2296-
auto cbegin() const -> iterator { return begin(); }
2297-
auto cend() const -> iterator { return end(); }
2298-
auto size() const -> std::size_t { return cpp2::unsafe_narrow<std::size_t>(ssize()); }
2299-
auto ssize() const -> int { return argc; }
2330+
auto begin() const -> iterator { return iterator{ argc, argv, 0 }; }
2331+
auto end() const -> iterator { return iterator{ argc, argv, argc }; }
2332+
auto cbegin() const -> iterator { return begin(); }
2333+
auto cend() const -> iterator { return end(); }
2334+
auto size() const -> std::size_t { return cpp2::unsafe_narrow<std::size_t>(ssize()); }
2335+
auto ssize() const -> std::ptrdiff_t { return argc; }
23002336

23012337
auto operator[](int i) const {
2302-
if (0 <= i && i < ssize()) { return std::string_view{ argv[i] }; }
2303-
else { return std::string_view{}; }
2338+
if (0 <= i && i < ssize()) { return std::string_view{ argv[i] }; }
2339+
else { return std::string_view{}; }
23042340
}
23052341

23062342
mutable int argc = 0; // mutable for compatibility with frameworks that take 'int& argc'
@@ -2704,9 +2740,16 @@ inline constexpr auto as_( auto&& x ) -> decltype(auto)
27042740
if constexpr (is_narrowing_v<C, CPP2_TYPEOF(x)>) {
27052741
static_assert(
27062742
program_violates_type_safety_guarantee<C, CPP2_TYPEOF(x)>,
2707-
"'as' does not allow unsafe narrowing conversions - if you're sure you want this, use `unsafe_narrow<T>()` to force the conversion"
2743+
"'as' does not allow unsafe possibly-lossy narrowing conversions - if you're sure you want this, use 'unsafe_narrow<T>' to explicitly force the conversion and possibly lose information"
27082744
);
27092745
}
2746+
else if constexpr (is_unsafe_pointer_conversion_v<C, CPP2_TYPEOF(x)>)
2747+
{
2748+
static_assert(
2749+
program_violates_type_safety_guarantee<C, CPP2_TYPEOF(x)>,
2750+
"'as' does not allow unsafe pointer conversions - if you're sure you want this, use `unsafe_cast<T>()` to explicitly force the unsafe cast"
2751+
);
2752+
}
27102753
else if constexpr( std::is_same_v< CPP2_TYPEOF(as<C>(CPP2_FORWARD(x))), nonesuch_ > ) {
27112754
static_assert(
27122755
program_violates_type_safety_guarantee<C, CPP2_TYPEOF(x)>,
@@ -2724,8 +2767,8 @@ inline constexpr auto as_() -> decltype(auto)
27242767
if constexpr( std::is_same_v< CPP2_TYPEOF((as<C, x>())), nonesuch_ > ) {
27252768
static_assert(
27262769
program_violates_type_safety_guarantee<C, CPP2_TYPEOF(x)>,
2727-
"Literal cannot be narrowed using 'as' - if you're sure you want this, use 'unsafe_narrow<T>()' to force the conversion"
2728-
);
2770+
"'as' does not allow unsafe possibly-lossy narrowing conversions - if you're sure you want this, use `unsafe_narrow<T>()` to explicitly force the conversion and possibly lose information"
2771+
);
27292772
}
27302773
}
27312774
else {

regression-tests/pure2-unsafe.cpp2

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
f: (i: i32, inout s: std::string) = {
3+
// j := i as i16; // error, maybe-lossy narrowing
4+
j := unsafe_narrow<i16>(i); // ok, 'unsafe' is explicit
5+
6+
pv: *void = s&;
7+
// pi := pv as *std::string; // error, unsafe cast
8+
ps := unsafe_cast<*std::string>(pv); // ok, 'unsafe' is explicit
9+
ps* = "plugh";
10+
}
11+
12+
main: () = {
13+
str: std::string = "xyzzy";
14+
f( 42, str );
15+
std::cout << str;
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plugh
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plugh
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
11
In file included from mixed-bugfix-for-ufcs-non-local.cpp:6:
22
../../../include/cpp2util.h:2100:1: error: lambda-expression in template parameter type
3-
2100 | constexpr auto is( std::optional<U> const& x ) -> bool
3+
2100 | //
44
| ^
55
../../../include/cpp2util.h:2137:59: note: in expansion of macro ‘CPP2_UFCS_’
6-
2137 | //
6+
2137 | // Value case
77
| ^
88
mixed-bugfix-for-ufcs-non-local.cpp2:13:12: note: in expansion of macro ‘CPP2_UFCS_NONLOCAL’
99
mixed-bugfix-for-ufcs-non-local.cpp2:13:36: error: template argument 1 is invalid
1010
../../../include/cpp2util.h:2100:1: error: lambda-expression in template parameter type
11-
2100 | constexpr auto is( std::optional<U> const& x ) -> bool
11+
2100 | //
1212
| ^
1313
../../../include/cpp2util.h:2137:59: note: in expansion of macro ‘CPP2_UFCS_’
14-
2137 | //
14+
2137 | // Value case
1515
| ^
1616
mixed-bugfix-for-ufcs-non-local.cpp2:21:12: note: in expansion of macro ‘CPP2_UFCS_NONLOCAL’
1717
mixed-bugfix-for-ufcs-non-local.cpp2:21:36: error: template argument 1 is invalid
1818
../../../include/cpp2util.h:2100:1: error: lambda-expression in template parameter type
19-
2100 | constexpr auto is( std::optional<U> const& x ) -> bool
19+
2100 | //
2020
| ^
2121
../../../include/cpp2util.h:2137:59: note: in expansion of macro ‘CPP2_UFCS_’
22-
2137 | //
22+
2137 | // Value case
2323
| ^
2424
mixed-bugfix-for-ufcs-non-local.cpp2:31:12: note: in expansion of macro ‘CPP2_UFCS_NONLOCAL’
2525
mixed-bugfix-for-ufcs-non-local.cpp2:31:36: error: template argument 1 is invalid
2626
../../../include/cpp2util.h:2100:1: error: lambda-expression in template parameter type
27-
2100 | constexpr auto is( std::optional<U> const& x ) -> bool
27+
2100 | //
2828
| ^
2929
../../../include/cpp2util.h:2137:59: note: in expansion of macro ‘CPP2_UFCS_’
30-
2137 | //
30+
2137 | // Value case
3131
| ^
3232
mixed-bugfix-for-ufcs-non-local.cpp2:33:12: note: in expansion of macro ‘CPP2_UFCS_NONLOCAL’
3333
mixed-bugfix-for-ufcs-non-local.cpp2:33:36: error: template argument 1 is invalid
3434
../../../include/cpp2util.h:2100:1: error: lambda-expression in template parameter type
35-
2100 | constexpr auto is( std::optional<U> const& x ) -> bool
35+
2100 | //
3636
| ^
3737
../../../include/cpp2util.h:2137:59: note: in expansion of macro ‘CPP2_UFCS_’
38-
2137 | //
38+
2137 | // Value case
3939
| ^
4040
mixed-bugfix-for-ufcs-non-local.cpp2:21:12: note: in expansion of macro ‘CPP2_UFCS_NONLOCAL’
4141
mixed-bugfix-for-ufcs-non-local.cpp2:21:36: error: template argument 1 is invalid
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plugh
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plugh
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pure2-unsafe.cpp

0 commit comments

Comments
 (0)