Skip to content

add std::array overloads for constructors, to_array and get - #57

Open
0xull wants to merge 3 commits into
rustfoundation:mainfrom
0xull:array-overloads
Open

0xull wants to merge 3 commits into
rustfoundation:mainfrom
0xull:array-overloads

Conversation

@0xull

@0xull 0xull commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

this pr adds rust wrappers for std::array<int, 3> overloads covering three method names, the constructors, to_array and get.

std::array constructors are aggregate initialization with 0 to N initializers, in addition with the copy and move constructors. all of them are implemented for N = 3, so the constructor set is complete. unlike forward_list, none of its overloads clash because std::array has no count or value constructors.

c++ has two zero-argument forms (a; and a{};), but only one of them can be a rust overload, so i used value-initialization.

the constructors are the std::array overloads that differ by more than const or &/&&, so i chose to use two non-member functions as examples to show that rule:

  • to_array overloads on & and &mut built-in arrays, matching the c++ lvalue and rvalue versions.
  • get takes the element index as a template argument, and because the macro doesn't support const generics, each index gets its own overload through an Index<I> tag type. all three indices were done for &, but only index 0 was done for &mut and by-value, since every other index works the same way. but for the const&& overload it can't be represented, because rust types can't be const.

i implemented get to use &'static like forward_list::front, and its test leaks the arrays the same way the forward_list test does.

i skipped the other std::array overloads, with the Scope section in the module doc listing each one with the reason.

ai use: i used claude (web) to get explanation across different parts of the codebase, especially the overload! implementation and understanding of how overloads dispatch.

the code implementation are mine, written after studying forward_list implementation, and std::array specs from c++.

after studying forward_list and the use of &mut for c++ move, i was curious to ask if there have been any discussion about offering rust style move (by value)? i understand this would be a mismatch between both languages, but i'm curious for case when the moved-from c++ object itself needs not to be used after it is moved. because i initially implemented move to take ownership of the other object.

closes #34

  1. Write Rust wrappers for that C++ type, for 3-5 different overloaded method names:
    • Create a new file named after your type in overloading-macros/cpp-overload-test/src/stdcpp
    • Wrap constructors first, writing tests to make sure the correct constructor is called
    • Explain any potential compatibility issues in a Limitations doc section
    • Explain usability issues in an Ergonomics doc section
    • Check that you have implemented all the overloads for each method name, or documented why you can't
    • Push your changes and wait for a review
  2. Add a separate example with some failing overloads
    • Create a new file named after your type in overloading-macros/cpp-overload-test/examples
    • Wrap accessor/modifier methods
    • Explain how they fail in an Incompatibilities doc section
    • Explain how the overload failures could be resolved in a Workarounds doc section
    • Optional: If there's a C++ feature with no equivalent in Rust, explain that in an Inexpressible Overloads doc section
    • Push your changes and wait for a review
  3. Update the overload macro compatibility table Markdown doc
    • If you haven't checked some kinds of overloads, put a ?in those squares
    • Push your changes and wait for a review

Signed-off-by: Ikeh Chukwuka <mrikehchukwuka@gmail.com>
0xull added 2 commits October 18, 2026 07:20
Signed-off-by: Ikeh Chukwuka <mrikehchukwuka@gmail.com>
…ex<I>` tag type

Signed-off-by: Ikeh Chukwuka <mrikehchukwuka@gmail.com>
@0xull 0xull changed the title add std::array constructor overloads and tests add std::array overloads for constructors, to_array and get Oct 19, 2026
@0xull
0xull marked this pull request as ready for review October 19, 2026 05:55

@thunderseethe thunderseethe left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start! Thanks for the submission.

after studying forward_list and the use of &mut for c++ move, i was curious to ask if there have been any discussion about offering rust style move (by value)? i understand this would be a mismatch between both languages, but i'm curious for case when the moved-from c++ object itself needs not to be used after it is moved. because i initially implemented move to take ownership of the other object.

I think this is definitely one of the more interesting questions about FFI between C++ and Rust. I would love to hear how you would tackle some of the problems for array. Especially around how to move items in and out of the array in both languages. Where Rust expects to be able to destructively move things out of an array that it owns, callers have to be careful not to leave an array element in an invalid state where Rust expects it won't be referenced again but C++ attemps to read it expecting the element to be left in a moved-from state.

I left one minor comment on section 1, but overall looks good. Can you show me an example of some failing overloads for array (per section 2)?

/// 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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For methods that take an array "by value" like this, how can we uphold the lifetime guarentees here? This won't really matter for this case where T is an int but what if T was an object with a destructor. How can we ensure that when we return T by value that T won't get dropped when we drop the array initially holding it. If we can't, can we document the invariants required of our array when we call this method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true for int, nothing is ever destroyed. but once T has a destructor, copying the element's bytes out the way ptr::read would breaks, because std::array's destructor always destroys every element and has no way to skip the one we took. i reproduced this with std::array<std::string, 3> and got a double free when the copied string was dropped after the array was deleted. the version that holds up runs T's move constructor in c++, keeps the new T on the c++ heap behind a pointer, and then deletes the array exactly once, which destroys the moved-from shell harmlessly.

thank you, i will update the by-value get's docs to account for these invariants for a generic T, and i'll keep documenting as my findings surfaces more.

@0xull

0xull commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

thank you for the review!

after taking some time to think it through, the rule i'd build on is that rust only ever moves pointers to c++ objects and not their bytes. which the array wrapper already does. moving a StdArray3 copies the pointer and the c++ array stays at one address.

for moving the whole array, the &mut constructor keeps c++ semantics and leaves the moved-from array with the caller. a by-value rust-style move is also sound if it move-constructs the new array and then deletes the source immediately, which is safe because rust guarantees nothing else can still see the source. the cost is that the caller can't reuse the moved-from array the way c++ code sometimes does, which i think answers what i was originally curious about.

for moving an element out, rust's own rules line up with c++ more than i expected. safe rust won't let you move out of a slot you only borrow, so you need mem::take or mem::replace, which leaves a valid value behind just as c++ requires every slot to stay a live object until the array is destroyed. for the by-value get (which owns the array) the ownership and dropping of elements can end up being a mixture. in my first example, c++ moves the element into a new object, which leaves a moved-from object in the slot, and then the whole array is destroyed in c++, the moved-from object included, while rust makes sure that destruction happens exactly once.

struct ValueArray {
    ptr: *mut c_void,
    note: Note, // a rust field with its own `Drop`
}

impl Drop for ValueArray {
    fn drop(&mut self) {
        let p = self.ptr;
        unsafe { cpp!([p as "std::array<Value, 3>*"] { delete p; }) }
    }
}

fn get_0_forget_self(self) -> ValueBox {
    let slf = self.ptr;
    std::mem::forget(self); // skips `Drop` for `ValueArray` and the drop of `note`
    let out = unsafe {
        cpp!([slf as "std::array<Value, 3>*"] -> *mut c_void as "Value*" {
            Value* out = new Value(std::get<0>(std::move(*slf)));
            delete slf;
            return out;
        })
    };
    ValueBox(out)
}

in this first example, forgetting self also skips dropping any other fields the wrapper has, so letting Drop do the delete is simpler:

fn get_0_move(self) -> ValueBox {
    let slf = self.ptr;
    let out = unsafe {
        cpp!([slf as "std::array<Value, 3>*"] -> *mut c_void as "Value*" {
            return new Value(std::get<0>(std::move(*slf))); // slot 0 now holds a moved-from object
        })
    };
    ValueBox(out) // its `Drop` deletes the new value once
    // `self` drops here, and its `Drop` deletes the array, the moved-from object included
}

for moving elements in, to_array(&mut items) leaves moved-from values in the caller's array. the test asserting they still equal [4, 5, 6] only holds because moving an int copies it, and with std::string the source came out empty in my run, so a generic version could only destroy or reassign those values.

the way i see the split, the c++ glue owns the move-then-destroy steps and any compile-time checks, and the rust types own the promise that rust never relocates a c++ object's bytes or drops one twice. i believe that is roughly how cxx divides it with its Opaque/Pin and moveit with MoveNew.

phew, i'll start working on step 2, plus the doc updates. thank you once again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

array

2 participants