From 6c6067553367f20f2baed27aeb2d5b760fa40dd3 Mon Sep 17 00:00:00 2001 From: Ikeh Chukwuka Date: Fri, 16 Oct 2026 13:15:52 +0100 Subject: [PATCH 1/5] add std::array constructor overloads and tests Signed-off-by: Ikeh Chukwuka --- cpp-overload-test/src/lib.rs | 1 + cpp-overload-test/src/stdcpp/array.rs | 221 ++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 cpp-overload-test/src/stdcpp/array.rs diff --git a/cpp-overload-test/src/lib.rs b/cpp-overload-test/src/lib.rs index b72552c..308b9a2 100644 --- a/cpp-overload-test/src/lib.rs +++ b/cpp-overload-test/src/lib.rs @@ -7,5 +7,6 @@ /// The C++ standard library. pub mod stdcpp { // Add new modules for each type here, in alphabetical order. + pub mod array; pub mod forward_list; } diff --git a/cpp-overload-test/src/stdcpp/array.rs b/cpp-overload-test/src/stdcpp/array.rs new file mode 100644 index 0000000..a364386 --- /dev/null +++ b/cpp-overload-test/src/stdcpp/array.rs @@ -0,0 +1,221 @@ +//! Test that C++ overloads for [std::array][array] can be successfully represented in Rust. +//! +//! `std::array` is an aggregate type, so it has no user-declared constructors. Instead, its +//! constructor overloads are aggregate initialization with up to `N` initializers, plus +//! the implicitly declared copy and move constructors. +//! +//! [array]: https://en.cppreference.com/cpp/container/array + +#![allow(unused_braces, reason = "Generated by overload! macro")] + +use cpp::cpp; +use splat_overload::overload; +use std::ffi::c_void; +use std::marker::PhantomData; + +// C++ header includes +cpp! {{ + #include + #include +}} + +/// Workaround for missing generic support in the `overload!` macro. +pub type T = std::ffi::c_int; + +/// Workaround for missing const generic support in the `overload!` macro. +/// +/// ### Limitations +/// +/// The C++ type strings in `cpp!` can't refer to this constant, so `3` is hardcoded. +/// Changing this value without changing every `std::array` string will make the Rust +/// and C++ sizes disagree. +pub const N: usize = 3; + +/// A wrapper struct to hold the returned C++ `std::array` pointer. +/// +/// The size is in the name, because arrays with different sizes are different C++ types. +/// +/// ### Limitations +/// +/// This struct and the impl should be generic over the element type and the size. +/// The size is a C++ non-type template parameter, which equivalent to a Rust const generic. +/// FIXME: make the `overload!` macro support generics, including const generics. +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +pub struct StdArray3(*mut c_void, PhantomData<[T; N]>); + +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +#[allow( + non_upper_case_globals, + reason = "Triggered by cpp! macro in rust-analyzer" +)] +impl StdArray3 { + // Constructors: internal C++ implementations for `new(...)` overloads + fn value_init() -> StdArray3 { + let array = unsafe { + cpp!([] -> *mut c_void as "std::array*" { + // The braces value-initialize every element in the array. + // Without them, `int` elements would be left uninitialized. + return new std::array{}; + }) + }; + StdArray3(array, PhantomData) + } + + fn from_initializer_list_1(a: T) -> StdArray3 { + let array = unsafe { + cpp!([a as "int"] -> *mut c_void as "std::array*" { + return new std::array{ a }; + }) + }; + StdArray3(array, PhantomData) + } + + fn from_initializer_list_2(a: T, b: T) -> StdArray3 { + let array = unsafe { + cpp!([a as "int", b as "int"] -> *mut c_void as "std::array*" { + return new std::array{ a, b }; + }) + }; + StdArray3(array, PhantomData) + } + + fn from_initializer_list_3(a: T, b: T, c: T) -> StdArray3 { + let array = unsafe { + cpp!([a as "int", b as "int", c as "int"] -> *mut c_void as "std::array*" { + return new std::array{ a, b, c }; + }) + }; + StdArray3(array, PhantomData) + } + + fn copy_from(other: &StdArray3) -> StdArray3 { + let other = other.0 as *const c_void; + let array = unsafe { + cpp!([other as "const std::array*"] -> *mut c_void as "std::array*" { + return new std::array(*other); + }) + }; + StdArray3(array, PhantomData) + } + + fn move_from(other: &mut StdArray3) -> StdArray3 { + let other: *mut c_void = other.0; + let array = unsafe { + cpp!([other as "std::array*"] -> *mut c_void as "std::array*" { + return new std::array(std::move(*other)); + }) + }; + StdArray3(array, PhantomData) + } + + // Array checks (not overloaded) + + /// Copy every element out of the array, so tests can check the whole array. + fn contents(&self) -> [T; N] { + let slf = self.0 as *const c_void; + let data = unsafe { + cpp!([slf as "const std::array*"] -> *const T as "const int*" { + return slf->data(); + }) + }; + // SAFETY: `data()` points to `N` contiguous, initialized `int` elements, having + // same layout as `[c_int; N]`. + unsafe { *(data as *const [T; N]) } + } +} + +// Constructors: `new(...)` +overload! { + impl StdArray3 { + /// Construct an array with every element value-initialized (zero for `int`). + /// + /// ### Limitations + /// + /// C++ has two zero-argument forms: `std::array a;` which leaves `int` elements + /// uninitialized, and `std::array a{};` that sets them to zero. C++ tells them + /// apart using syntax, not their argument types. We chose value-initialization, + /// because reading uninitialized elements is undefined behaviour. + pub fn new() -> StdArray3 { + StdArray3::value_init() + } + + /// Construct an array from 1 item, and value-initialize the other elements. + /// + /// ### Limitations + /// + /// C++ accepts up to `N` initializers and rejects more at compile time. Here, + /// `new(a, b, c, d)` fails with a missing overload error. A generic implementation + /// would need a compile-time check that the number of arguments is at most `N`. + /// + /// C++ value-initializes the missing elements, which works like a default argument value + /// for each element. Rust has no default argument values, so each initializer count needs + /// its own overload. + /// + /// These overloads don't clash with any other constructor, because `std::array` doesn't + /// have `count` or `value` constructors. + pub fn new(a: T) -> StdArray3 { + StdArray3::from_initializer_list_1(a) + } + + /// Construct an array from 2 items, and value-initialize the last element. + pub fn new(a: T, b: T) -> StdArray3 { + StdArray3::from_initializer_list_2(a, b) + } + + /// Construct an array from 3 items. + pub fn new(a: T, b: T, c: T) -> StdArray3 { + StdArray3::from_initializer_list_3(a, b, c) + } + + /// Copy the elements of another array. + pub fn new(other: &StdArray3) -> StdArray3 { + StdArray3::copy_from(other) + } + + /// Move the elements of another array. + /// + /// ### Ergonomics + /// + /// This takes `&mut` so the caller still has the old array after the move. + /// That matches C++, but Rust users usually expect a move to give the value away, + /// which taking the array by value would do. + /// + /// ### Limitations + /// + /// In C++, moving an `int` copies it and leaves the source unchanged, so the C++ + /// copy and move constructors produce identical arrays, and tests can't tell + /// which C++ constructor ran. + pub fn new(other: &mut StdArray3) -> StdArray3 { + StdArray3::move_from(other) + } + } +} + +/// These tests show that the correct Rust and C++ overloads are being called. +#[test] +pub fn test_array() { + // Constructors: `new(...)` + // + // Each constructor call produces different contents, so a call that dispatched to the wrong + // overload would fail its assertion. + let value_init = StdArray3::new(); + assert_eq!(value_init.contents(), [0, 0, 0]); + + let from_initializer_list = StdArray3::new(7); + assert_eq!(from_initializer_list.contents(), [7, 0, 0]); + let from_initializer_list = StdArray3::new(7, 8); + assert_eq!(from_initializer_list.contents(), [7, 8, 0]); + let from_initializer_list = StdArray3::new(7, 8, 9); + assert_eq!(from_initializer_list.contents(), [7, 8, 9]); + + let copied = StdArray3::new(&from_initializer_list); + assert_eq!(copied.contents(), [7, 8, 9]); + // Check that the original array is still usable. + assert_eq!(from_initializer_list.contents(), [7, 8, 9]); + + let mut to_move = StdArray3::new(4, 5, 6); + let moved = StdArray3::new(&mut to_move); + assert_eq!(moved.contents(), [4, 5, 6]); + // Moving `int` elements leaves them unchanged, so the moved-from array still holds them. + assert_eq!(to_move.contents(), [4, 5, 6]); +} From 8f53a40871a1ed5339e92ff7a72458ffd97739ab Mon Sep 17 00:00:00 2001 From: Ikeh Chukwuka Date: Sun, 18 Oct 2026 07:20:23 +0100 Subject: [PATCH 2/5] add std::to_array overloads and tests Signed-off-by: Ikeh Chukwuka --- cpp-overload-test/src/stdcpp/array.rs | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/cpp-overload-test/src/stdcpp/array.rs b/cpp-overload-test/src/stdcpp/array.rs index a364386..7dea527 100644 --- a/cpp-overload-test/src/stdcpp/array.rs +++ b/cpp-overload-test/src/stdcpp/array.rs @@ -17,6 +17,10 @@ use std::marker::PhantomData; cpp! {{ #include #include + + // The `cpp!` macro failed to compile for array type syntax like `int(*)[3]`, + // so we have to declare the built-in array type here. + typedef int IntArray3[3]; }} /// Workaround for missing generic support in the `overload!` macro. @@ -108,6 +112,35 @@ impl StdArray3 { StdArray3(array, PhantomData) } + // Non-member functions + + // internal C++ implementations for `to_array(...)` overloads + fn to_array_copy(items: &[T; N]) -> StdArray3 { + let items: *const [T; N] = items; + // SAFETY: `items` points to `N` initialized elements that stay borrowed until the C++ call + // returns, and `N` matches the size of `IntArray3`. + let array = unsafe { + cpp!([items as "const IntArray3*"] -> *mut c_void as "std::array*" { + // `*items` is an lvalue, so this calls the `T(&)[N]` overload. + return new std::array(std::to_array(*items)); + }) + }; + StdArray3(array, PhantomData) + } + + fn to_array_move(items: &mut [T; N]) -> StdArray3 { + let items: *mut [T; N] = items; + // SAFETY: `items` points to `N` initialized elements that stay mutably borrowed until the + // C++ call returns, and `N` matches the size of `IntArray3`. + let array = unsafe { + cpp!([items as "IntArray3*"] -> *mut c_void as "std::array*" { + // `std::move(*items)` is an rvalue, hence calls the `T(&&)[N]` overload. + return new std::array(std::to_array(std::move(*items))); + }) + }; + StdArray3(array, PhantomData) + } + // Array checks (not overloaded) /// Copy every element out of the array, so tests can check the whole array. @@ -191,6 +224,26 @@ overload! { } } +// Non-member functions: `to_array(...)` +overload! { + /// Create an array by copying the elements of a built-in array. + /// + /// ### Limitations + /// + /// C++ uses `T(&)[N]` and `T(&&)[N]` to tell lvalue and rvalue arrays apart. Like the move + /// constructor, the rvalue overload takes `&mut [T; N]`, because the caller still owns the + /// moved-from array in C++. For `int` elements, copying and moving have the same effect, + /// meaning tests can't tell which C++ overload was called. + fn to_array(items: &[T; N]) -> StdArray3 { + StdArray3::to_array_copy(items) + } + + /// Create an array by moving the elements of a built-in array. + fn to_array(items: &mut [T; N]) -> StdArray3 { + StdArray3::to_array_move(items) + } +} + /// These tests show that the correct Rust and C++ overloads are being called. #[test] pub fn test_array() { @@ -218,4 +271,13 @@ pub fn test_array() { assert_eq!(moved.contents(), [4, 5, 6]); // Moving `int` elements leaves them unchanged, so the moved-from array still holds them. assert_eq!(to_move.contents(), [4, 5, 6]); + + // Non-member functions: `to_array(...)` + let items: [T; N] = [1, 2, 3]; + let copied_items = to_array(&items); + assert_eq!(copied_items.contents(), [1, 2, 3]); + let mut items_to_move: [T; N] = [4, 5, 6]; + let moved_items = to_array(&mut items_to_move); + assert_eq!(moved_items.contents(), [4, 5, 6]); + assert_eq!(items_to_move, [4, 5, 6]); } From c3a067a5ca12ee1b2b24dcba6d97c0c2a7f61548 Mon Sep 17 00:00:00 2001 From: Ikeh Chukwuka Date: Mon, 19 Oct 2026 05:24:24 +0100 Subject: [PATCH 3/5] add `get` overloads for `&`, `&mut` and by-value arrays using an `Index` tag type Signed-off-by: Ikeh Chukwuka --- cpp-overload-test/src/stdcpp/array.rs | 142 ++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/cpp-overload-test/src/stdcpp/array.rs b/cpp-overload-test/src/stdcpp/array.rs index 7dea527..4e84ba7 100644 --- a/cpp-overload-test/src/stdcpp/array.rs +++ b/cpp-overload-test/src/stdcpp/array.rs @@ -4,6 +4,20 @@ //! constructor overloads are aggregate initialization with up to `N` initializers, plus //! the implicitly declared copy and move constructors. //! +//! ### Scope +//! +//! The constructors are the only `std::array` overloads that differ by more than `const` or +//! `&`/`&&`. We also wrap two non-member functions as examples of overloads that only differ by +//! `const` or `&`/`&&`, because each one adds something new: +//! - `get` takes the element index as a compile-time template argument +//! - `to_array` overloads on lvalue and rvalue built-in arrays +//! +//! These overloads are skipped because: +//! - `at`, `front`, `back`, `data`, `begin`, `end`, `rbegin`, and `rend` only differ by `const` +//! - `operator[]` and the comparison operators are already possible in Rust +//! - `operator=` have no equivalent in Rust +//! - `fill`, `size`, `swap`, `empty`, and `max_size` are not overloaded +//! //! [array]: https://en.cppreference.com/cpp/container/array #![allow(unused_braces, reason = "Generated by overload! macro")] @@ -47,6 +61,14 @@ pub const N: usize = 3; #[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] pub struct StdArray3(*mut c_void, PhantomData<[T; N]>); +/// A tag type that carries a compile-time array index, like the `I` in C++ `std::get`. +/// +/// ### Workarounds +/// +/// The `overload!` macro doesn't support const generics, so `get` can't be generic over the +/// index. Here, each overload takes a different concrete `Index` type. +pub struct Index; + #[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] #[allow( non_upper_case_globals, @@ -141,6 +163,61 @@ impl StdArray3 { StdArray3(array, PhantomData) } + // internal C++ implementations for `get(...)` overloads + fn get_0_const(&self) -> &T { + let slf = self.0 as *const c_void; + let element = unsafe { + cpp!([slf as "const std::array*"] -> *const T as "const int*" { + return &std::get<0>(*slf); + }) + }; + // SAFETY: `element` is valid for as long as `self` is borrowed. + unsafe { &*element } + } + + fn get_1_const(&self) -> &T { + let slf = self.0 as *const c_void; + let element = unsafe { + cpp!([slf as "const std::array*"] -> *const T as "const int*" { + return &std::get<1>(*slf); + }) + }; + // SAFETY: `element` is valid for as long as `self` is borrowed. + unsafe { &*element } + } + + fn get_2_const(&self) -> &T { + let slf = self.0 as *const c_void; + let element = unsafe { + cpp!([slf as "const std::array*"] -> *const T as "const int*" { + return &std::get<2>(*slf); + }) + }; + // SAFETY: `element` is valid for as long as `self` is borrowed. + unsafe { &*element } + } + + fn get_0_mut(&mut self) -> &mut T { + let slf: *mut c_void = self.0; + let element = unsafe { + cpp!([slf as "std::array*"] -> *mut T as "int*" { + return &std::get<0>(*slf); + }) + }; + // SAFETY: `self` is mutably borrowed, so nothing else can access the element + // while the returned reference exists. + unsafe { &mut *element } + } + + fn get_0_move(self) -> T { + let slf: *mut c_void = self.0; + unsafe { + cpp!([slf as "std::array*"] -> T as "int" { + return std::get<0>(std::move(*slf)); + }) + } + } + // Array checks (not overloaded) /// Copy every element out of the array, so tests can check the whole array. @@ -244,6 +321,56 @@ overload! { } } +// Non-member functions: `get(...)` +overload! { + /// Get a constant reference to element 0. + /// + /// ### Limitations + /// + /// C++ `std::get(array)` takes the index as a template argument, so an index that is out of + /// range is a compile error. Because `overload!` macro doesn't support const generics, + /// each index uses the `Index` tag type for its own overload. + /// + /// These overloads use `&'static` because elided lifetimes fail with + /// "missing lifetime in associated type" in the code the macro generates, and the macro + /// doesn't support generic lifetimes. + fn get(array: &'static StdArray3, _index: Index<0>) -> &'static T { + array.get_0_const() + } + + /// Get a constant reference to element 1. + fn get(array: &'static StdArray3, _index: Index<1>) -> &'static T { + array.get_1_const() + } + + /// Get a constant reference to element 2. + fn get(array: &'static StdArray3, _index: Index<2>) -> &'static T { + array.get_2_const() + } + + /// Get a mutable reference to element 0. + /// + /// ### Limitations + /// + /// Implementing only the mutable and moving overloads for index 0 here, because + /// every index works the same way. hence is enough to show that the `&`, `&mut`, + /// and by-value overloads can coexist. + fn get(array: &'static mut StdArray3, _index: Index<0>) -> &'static mut T { + array.get_0_mut() + } + + /// Move element 0 out of the array. + /// + /// ### Limitations + /// + /// C++ also has a `const T&& get(const std::array&&)` overload but Rust types can't be `const`, + /// so a constant rvalue array has the same Rust type as a moved array. That overload + /// can't be represented separately from this one. + fn get(array: StdArray3, _index: Index<0>) -> T { + array.get_0_move() + } +} + /// These tests show that the correct Rust and C++ overloads are being called. #[test] pub fn test_array() { @@ -280,4 +407,19 @@ pub fn test_array() { let moved_items = to_array(&mut items_to_move); assert_eq!(moved_items.contents(), [4, 5, 6]); assert_eq!(items_to_move, [4, 5, 6]); + + // Non-member functions: `get(...)` + // + // ### Workaround + // + // Leak these arrays to get static references to them. This would never work in production. + let static_ref = |array: StdArray3| -> &'static StdArray3 { &*Box::leak(Box::new(array)) }; + let static_mut = |array: StdArray3| -> &'static mut StdArray3 { Box::leak(Box::new(array)) }; + + let get_array = static_ref(StdArray3::new(7, 8, 9)); + assert_eq!(get(get_array, Index::<0>), &7); + assert_eq!(get(get_array, Index::<1>), &8); + assert_eq!(get(get_array, Index::<2>), &9); + assert_eq!(get(static_mut(StdArray3::new(7, 8, 9)), Index::<0>), &mut 7); + assert_eq!(get(StdArray3::new(7, 8, 9), Index::<0>), 7); } From 4f6a980dcccc8423e6d07fba0880d70d829aa042 Mon Sep 17 00:00:00 2001 From: Ikeh Chukwuka Date: Sun, 25 Oct 2026 05:24:37 +0100 Subject: [PATCH 4/5] add std::array failing example and document ffi limits Signed-off-by: Ikeh Chukwuka --- cpp-overload-test/examples/stdcpp-array.rs | 139 ++++++++++++++++++ .../examples/stdcpp-array.stderr | 78 ++++++++++ cpp-overload-test/src/stdcpp/array.rs | 109 ++++++++------ 3 files changed, 283 insertions(+), 43 deletions(-) create mode 100644 cpp-overload-test/examples/stdcpp-array.rs create mode 100644 cpp-overload-test/examples/stdcpp-array.stderr diff --git a/cpp-overload-test/examples/stdcpp-array.rs b/cpp-overload-test/examples/stdcpp-array.rs new file mode 100644 index 0000000..b29b1b1 --- /dev/null +++ b/cpp-overload-test/examples/stdcpp-array.rs @@ -0,0 +1,139 @@ +//! Test C++ [std::array][array] overloads that can't be represented in Rust, and record the macro diagnostics. +//! Error output is in [stdcpp-array.stderr]. +//! Working overloads are in [array.rs]. +//! +//! [array]: https://en.cppreference.com/cpp/container/array +//! [stdcpp-array.stderr]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/examples/stdcpp-array.stderr +//! [array.rs]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/array.rs +//! +//! The `cpp!` macro doesn't work here, because of technical limitations it can only run one build +//! per crate. (Examples and binaries are built separately from `lib.rs`.) +//! To add C++ code here, use /* ... */ code comment blocks, or just skip it. +//! +//! `std::array`'s modifiers, `fill` and `swap`, aren't overloaded, so they have no failing +//! overloads. +//! +//! ### Inexpressible Overloads +//! +//! C++ has two ways to create an array with no arguments. `std::array a;` leaves the `int` +//! elements uninitialized, and `std::array a{};` sets them to zero. C++ chooses between +//! them using syntax, not their argument types, so Rust overloads can only represent one of them. +//! +//! C++ class template argument deduction (CTAD) lets `std::array a{1, 2, 3};` work out `N` from +//! the number of initializers. A Rust associated function like `new` belongs to one concrete type, +//! so it can't pick `N`. A free function overload set could return a different array size for each +//! argument count, but until the macro supports const generics, each size needs its own wrapper +//! type. + +#![feature(splat, tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; + +/// Workaround for missing generic support in the `overload!` macro. +type T = std::ffi::c_int; + +/// A wrapper struct to hold the C++ array, with the `T` fake generic type and a real const +/// generic size. We fake the C++ array pointer with a Rust array to help with type checking. +#[expect(dead_code, reason = "Incompatible overloads can't be used")] +struct StdArrayN([T; N]); + +// Constructors: `new(...)` for any array size. +// +// ### Incompatibilities +// +// In C++ the array size `N` is a non-type template parameter, which maps to a Rust const +// generic. `overload!` drops the generics on the `impl` block, and keeps only the last +// name in the type path, so the generated code refers to `StdArrayN` without its size. +// +// ### Workarounds +// +// Use a fixed size, like `StdArray3` in `array.rs`, until +// the macro supports generics. +// +// ### Ergonomics +// +// The compiler's suggestions describe the code the macro generates, not this source. It suggests +// replacing `N` with `T` and adding a second `` after `StdArrayN`, and neither of those +// helps. +// +// FIXME: make `overload!` keep the `impl` generics, or report a clear error when it drops them. +// +// These notes can't be doc comments on the `impl` block, because the macro only +// recognises an `impl` block when `impl` is its first token. +overload! { + impl StdArrayN { + /// Construct an array with every element value-initialized. + fn new() -> StdArrayN { + todo!() + } + + /// Construct an array from 1 item, and value-initialize the other elements. + fn new(_a: T) -> StdArrayN { + todo!() + } + } +} + +/// A wrapper struct to hold the C++ `std::array`, with the `T` fake generic type. +/// We fake the C++ array pointer with a Rust array to help with type checking. +#[expect(dead_code, reason = "Incompatible overloads can't be used")] +struct StdArray3([T; 3]); + +// Accessors: `at(...)` +overload! { + impl StdArray3 { + /// Get a mutable reference to the element at `pos`. + /// + /// ### Incompatibilities + /// + /// C++ overloads `at` on whether the array is `const`, which maps to Rust `&self` + /// and `&mut self`. `overload!` can't overload on the receiver, because it + /// dispatches every overload through a single function, which can only have one receiver + /// type. `front`, `back`, `data`, `begin`, `end`, `rbegin`, and `rend` fail in the same + /// way. + /// + /// ### Workarounds + /// + /// Pass the array as an ordinary argument instead of `self`, like `get` in + /// `array.rs`. `&StdArray3` and `&mut StdArray3` are different types, so they can be + /// overloaded. + fn at(&mut self, _pos: usize) -> &mut T { + todo!() + } + + /// Get a constant reference to the element at `pos`. + fn at(&self, _pos: usize) -> &T { + todo!() + } + } +} + +// Non-member functions: `get(...)` +overload! { + /// Get a mutable reference to element `I`. + /// + /// ### Incompatibilities + /// + /// C++ `std::get(array)` takes the element index as a template argument, which maps to a + /// Rust const generic. `overload!` drops each function's generics, so `I` doesn't exist in the + /// generated code. Even with generics, the generated `get` function is only generic over its + /// argument types, so a call like `get::<0>(array)` has nowhere to put the index. + /// + /// ### Workarounds + /// + /// Pass the index as a value of a zero-sized tag type, `Index`, and write one overload for + /// each index. This is similar to C++ tag dispatch, and `array.rs` uses it for `get`. + /// A mature overloading feature should support const generic arguments instead. + fn get(array: &'static mut StdArray3) -> &'static mut T { + &mut array.0[I] + } + + /// Get a constant reference to element `I`. + fn get(array: &'static StdArray3) -> &'static T { + &array.0[I] + } +} + +pub fn main() {} diff --git a/cpp-overload-test/examples/stdcpp-array.stderr b/cpp-overload-test/examples/stdcpp-array.stderr new file mode 100644 index 0000000..3010751 --- /dev/null +++ b/cpp-overload-test/examples/stdcpp-array.stderr @@ -0,0 +1,78 @@ +error: proc macro panicked + --> cpp-overload-test/examples/stdcpp-array.rs:85:1 + | + 85 | / overload! { + 86 | | impl StdArray3 { + 87 | | /// Get a mutable reference to the element at `pos`. + 88 | | /// +... | +111 | | } + | |_^ + | + = help: message: all overloads must have the same receiver kind (&self, &mut self, self, or no receiver), got at: &mut self and at: &self + +error[E0425]: cannot find type `N` in this scope + --> cpp-overload-test/examples/stdcpp-array.rs:68:31 + | +68 | fn new() -> StdArrayN { + | ^ not found in this scope + | +note: similarly named type alias `T` defined here + --> cpp-overload-test/examples/stdcpp-array.rs:35:1 + | +35 | type T = std::ffi::c_int; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +help: a type alias with a similar name exists + | +68 - fn new() -> StdArrayN { +68 + fn new() -> StdArrayN { + | + +error[E0425]: cannot find type `N` in this scope + --> cpp-overload-test/examples/stdcpp-array.rs:73:36 + | +73 | fn new(_a: T) -> StdArrayN { + | ^ not found in this scope + | +note: similarly named type alias `T` defined here + --> cpp-overload-test/examples/stdcpp-array.rs:35:1 + | +35 | type T = std::ffi::c_int; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +help: a type alias with a similar name exists + | +73 - fn new(_a: T) -> StdArrayN { +73 + fn new(_a: T) -> StdArrayN { + | + +error[E0425]: cannot find value `I` in this scope + --> cpp-overload-test/examples/stdcpp-array.rs:130:22 + | +130 | &mut array.0[I] + | ^ not found in this scope + +error[E0425]: cannot find value `I` in this scope + --> cpp-overload-test/examples/stdcpp-array.rs:135:18 + | +135 | &array.0[I] + | ^ not found in this scope + +error[E0107]: missing generics for struct `StdArrayN` + --> cpp-overload-test/examples/stdcpp-array.rs:66:26 + | +66 | impl StdArrayN { + | ^^^^^^^^^ expected 1 generic argument + | +note: struct defined here, with 1 generic parameter: `N` + --> cpp-overload-test/examples/stdcpp-array.rs:40:8 + | +40 | struct StdArrayN([T; N]); + | ^^^^^^^^^ -------------- +help: add missing generic argument + | +66 | impl StdArrayN { + | +++ + +Some errors have detailed explanations: E0107, E0425. +For more information about an error, try `rustc --explain E0107`. +error: could not compile `cpp-overload-test` (example "stdcpp-array") due to 6 previous errors diff --git a/cpp-overload-test/src/stdcpp/array.rs b/cpp-overload-test/src/stdcpp/array.rs index 4e84ba7..d42f20a 100644 --- a/cpp-overload-test/src/stdcpp/array.rs +++ b/cpp-overload-test/src/stdcpp/array.rs @@ -1,4 +1,5 @@ //! Test that C++ overloads for [std::array][array] can be successfully represented in Rust. +//! Failing overloads are in [stdcpp-array.rs], and error output is in [stdcpp-array.stderr]. //! //! `std::array` is an aggregate type, so it has no user-declared constructors. Instead, its //! constructor overloads are aggregate initialization with up to `N` initializers, plus @@ -19,6 +20,8 @@ //! - `fill`, `size`, `swap`, `empty`, and `max_size` are not overloaded //! //! [array]: https://en.cppreference.com/cpp/container/array +//! [stdcpp-array.rs]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/examples/stdcpp-array.rs +//! [stdcpp-array.stderr]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/examples/stdcpp-array.stderr #![allow(unused_braces, reason = "Generated by overload! macro")] @@ -58,6 +61,17 @@ pub const N: usize = 3; /// This struct and the impl should be generic over the element type and the size. /// The size is a C++ non-type template parameter, which equivalent to a Rust const generic. /// FIXME: make the `overload!` macro support generics, including const generics. +/// +/// A generic version would also need the C++ element type to have the same size and alignment +/// as `T`, because `contents` reads the C++ elements as a `[T; N]`. Rust zero-sized types can +/// never meet that requirement, since a C++ array element always has a nonzero size. +/// +/// Every function that creates an array, which means the `new` and `to_array` overloads, +/// allocates it with C++ `new`, and C++ `new` throws `std::bad_alloc` when the allocation fails. +/// The `cpp!` macro calls into C++ through `extern "C"` functions, and a C++ exception unwinding +/// into Rust through an `extern "C"` call is undefined behaviour. For `int` elements, using +/// `new (std::nothrow)` with a null check would avoid this, because the allocation is the only +/// thing that can fail. #[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] pub struct StdArray3(*mut c_void, PhantomData<[T; N]>); @@ -164,6 +178,27 @@ impl StdArray3 { } // internal C++ implementations for `get(...)` overloads + fn get_0_mut(&mut self) -> &mut T { + let slf: *mut c_void = self.0; + let element = unsafe { + cpp!([slf as "std::array*"] -> *mut T as "int*" { + return &std::get<0>(*slf); + }) + }; + // SAFETY: `self` is mutably borrowed, so nothing else can access the element + // while the returned reference exists. + unsafe { &mut *element } + } + + fn get_0_move(self) -> T { + let slf: *mut c_void = self.0; + unsafe { + cpp!([slf as "std::array*"] -> T as "int" { + return std::get<0>(std::move(*slf)); + }) + } + } + fn get_0_const(&self) -> &T { let slf = self.0 as *const c_void; let element = unsafe { @@ -197,27 +232,6 @@ impl StdArray3 { unsafe { &*element } } - fn get_0_mut(&mut self) -> &mut T { - let slf: *mut c_void = self.0; - let element = unsafe { - cpp!([slf as "std::array*"] -> *mut T as "int*" { - return &std::get<0>(*slf); - }) - }; - // SAFETY: `self` is mutably borrowed, so nothing else can access the element - // while the returned reference exists. - unsafe { &mut *element } - } - - fn get_0_move(self) -> T { - let slf: *mut c_void = self.0; - unsafe { - cpp!([slf as "std::array*"] -> T as "int" { - return std::get<0>(std::move(*slf)); - }) - } - } - // Array checks (not overloaded) /// Copy every element out of the array, so tests can check the whole array. @@ -323,37 +337,19 @@ overload! { // Non-member functions: `get(...)` overload! { - /// Get a constant reference to element 0. + /// Get a mutable reference to element 0. /// /// ### Limitations /// /// C++ `std::get(array)` takes the index as a template argument, so an index that is out of - /// range is a compile error. Because `overload!` macro doesn't support const generics, - /// each index uses the `Index` tag type for its own overload. + /// range is a compile error. /// /// These overloads use `&'static` because elided lifetimes fail with /// "missing lifetime in associated type" in the code the macro generates, and the macro /// doesn't support generic lifetimes. - fn get(array: &'static StdArray3, _index: Index<0>) -> &'static T { - array.get_0_const() - } - - /// Get a constant reference to element 1. - fn get(array: &'static StdArray3, _index: Index<1>) -> &'static T { - array.get_1_const() - } - - /// Get a constant reference to element 2. - fn get(array: &'static StdArray3, _index: Index<2>) -> &'static T { - array.get_2_const() - } - - /// Get a mutable reference to element 0. - /// - /// ### Limitations /// /// Implementing only the mutable and moving overloads for index 0 here, because - /// every index works the same way. hence is enough to show that the `&`, `&mut`, + /// every index works the same way, which is enough to show that the `&`, `&mut`, /// and by-value overloads can coexist. fn get(array: &'static mut StdArray3, _index: Index<0>) -> &'static mut T { array.get_0_mut() @@ -364,11 +360,38 @@ overload! { /// ### Limitations /// /// C++ also has a `const T&& get(const std::array&&)` overload but Rust types can't be `const`, - /// so a constant rvalue array has the same Rust type as a moved array. That overload + /// so a constant rvalue array has the same Rust type as a moved array. That overload /// can't be represented separately from this one. + /// + /// This is only correct because `T` is `int`. C++ copies element 0 into a new `int`, and the + /// array is never freed, since `StdArray3` doesn't implement `Drop`. A generic version would + /// need to uphold these invariants: + /// - The returned value must be built by `T`'s move constructor and not by copying the + /// element's bytes, because the array still owns element 0 and will destroy it later. + /// - The array must be destroyed exactly once after the move, and that includes the + /// moved-from element. + /// - If Rust holds the returned `T` by value, `T` must be safe to move with `memcpy`, because + /// Rust moves values by copying their bytes. If it isn't, the value has to stay on the C++ + /// heap. + /// - `T`'s move constructor must not throw, because C++ exceptions can't cross into Rust. fn get(array: StdArray3, _index: Index<0>) -> T { array.get_0_move() } + + /// Get a constant reference to element 0. + fn get(array: &'static StdArray3, _index: Index<0>) -> &'static T { + array.get_0_const() + } + + /// Get a constant reference to element 1. + fn get(array: &'static StdArray3, _index: Index<1>) -> &'static T { + array.get_1_const() + } + + /// Get a constant reference to element 2. + fn get(array: &'static StdArray3, _index: Index<2>) -> &'static T { + array.get_2_const() + } } /// These tests show that the correct Rust and C++ overloads are being called. From 7b0fa3c3b915eb66be70dd76db54411019478acb Mon Sep 17 00:00:00 2001 From: Ikeh Chukwuka Date: Sat, 31 Oct 2026 09:56:03 +0100 Subject: [PATCH 5/5] add std::array to the summary doc Signed-off-by: Ikeh Chukwuka --- cpp-overloads-in-rust.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cpp-overloads-in-rust.md b/cpp-overloads-in-rust.md index dd22d16..550a377 100644 --- a/cpp-overloads-in-rust.md +++ b/cpp-overloads-in-rust.md @@ -26,6 +26,7 @@ See the Limitations section below the table for more details. | *Non-Template Classes* | Constructors | Accessors | Modifiers | Transforms | | TODO | ? | ? | ? | ? | | *Template Classes* | Constructors | Accessors | Modifiers | Transforms | +| [array] | 🌗⛔⚙️[^3] | 🌗⚙️[^4] | | | | [forward_list] | ✅ | 🌗⚙️[^1] | 🌗⚙️?[^2] | ✅ | | TODO | ? | ? | ? | ? | @@ -34,10 +35,18 @@ Accessors: Get information about the class, called as `instance->get(...)`
Modifiers: Add or change the items in a list class, called as `instance->put(...)`
Transforms: Rearrange the items in a list class, called as `instance->sort(...)`
+[array]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/array.rs [forward_list]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/forward_list.rs [^1]: Rust and C++ iterators have different semantics, using adapter types is an ergonomic workaround. [^2]: `insert_after` and `splice_after` still need to be analysed. +[^3]: A wrapper type must be provided for each array size, since the macro fails to retain const + generics. As both C++ default-initialisation and value-initialisation take no arguments, only + value-initialisation is supported. +[^4]: The Rust `get` takes the index as a tag type argument, with one overload for each index. + The overloads that return a reference require `'static` lifetimes, since the macro eliminates + lifetimes. `at`, `front`, and the other accessors which differ only in being `const` cannot + overload on `self`. ## Limitations @@ -53,6 +62,8 @@ Overload macro missing functionality: - this can be worked around using associated functions, but the ergonomics suffer - Generic types: - this makes representing C++ templated types with generic Rust types impossible + - const generics are also omitted, so the size `N` in `std::array` and the index `I` in + `std::get` cannot be generic - doesn't support `impl Iterator` or `where I: Iterator` as arguments, but supports `&dyn Iterator` (which is less ergonomic) - Lifetimes: @@ -71,6 +82,8 @@ Some overloads are confusing or hard to use, so we might not want to represent t - this can be resolved by skipping overloads for initializer lists - `&dyn Iterator` overloads require a type cast - this could be resolved with improvements to the macro, or with an iterator adapter type +- since C++ move constructors leave the source object in a valid state, the Rust overload takes + `&mut` rather than taking the value, something that Rust users do not expect from a move Type inference doesn't work on some overloads: this is likely a macro or compiler experiment limitation. @@ -101,6 +114,11 @@ These overloads have no equivalent in Rust: - legacy iterator pointer-like methods for various positions (`before_begin`/`begin`/`end`) - initializer list methods: these are also a common source of overload conflicts for C++ templated lists +- the cases in which C++ selects overloads based on syntax rather than on the argument types, for + example the default-initialisation of `std::array` (`std::array a;`) and + value-initialisation (`a{}`) +- class template argument deduction (CTAD), which determines a template argument such as `N` from + the constructor arguments #### Equivalences & Supersets