Array Internals
Understand the storage, range validation, copying, invalidation, and lifetime contracts behind every Sharp Runtime array-shaped representation.
There is no single Sharp Runtime array representation
Different native representations solve different ownership problems. Most one-dimensional owned sequences use std::vector<T>. System::Array supplies static algorithms over vectors and selected raw buffers. Span, ReadOnlySpan, Memory, ReadOnlyMemory, and ArraySegment are borrowed contiguous views. Collection classes add resizing and higher-level contracts.
| Representation | Owns elements/storage? | Can resize? | Primary use |
|---|---|---|---|
Native T[N] / std::array<T,N> | Yes, inline | No | Compile-time fixed native shape |
std::vector<T> | Yes | Yes | Default owned one-dimensional sequence |
System::Array | No instance | Algorithms can resize a vector | Familiar sort/copy/search/clear behavior |
Span<T> | No | No | Mutable pointer-and-length borrow |
ReadOnlySpan<T> | No | No | Read-only contiguous borrow |
Memory<T>/ReadOnlyMemory<T> | No in this implementation | No | Borrowed vector region that can produce a span |
ArraySegment<T> | No | No | Offset/count view into a vector |
List<T>-shaped collection | Yes | Yes | Collection API, versioning, enumerator behavior |
Native C++ arrays
A built-in array or std::array owns a fixed number of inline elements. It is useful for stack or member storage whose extent is known at compile time. It does not automatically participate in System::Array vector overloads. Converting to a span is a natural way to pass a bounded view without copying.
A raw pointer alone is not an array contract. It carries no capacity or ownership. Raw-pointer overloads in the runtime can validate signed indices and null where relevant, but they cannot prove that a caller supplied enough elements. Prefer a vector or span at new boundaries.
std::vector as the owned array
std::vector<T> owns a contiguous sequence. Its size is the number of constructed elements; capacity is reserved storage and is not permission to index beyond size. Growing beyond capacity relocates the buffer. Erasing or inserting can move elements and invalidate references even without a capacity change.
Element semantics come from T. A vector of values copies or moves values. A vector of unique_ptr is move-only and owns pointees exclusively. A vector of shared_ptr shares pointees. A vector of raw pointers owns nothing unless an external convention says otherwise.
System::Array is an algorithm class
System::Array deletes construction. Its templates operate on std::vector<T> and, for copying, raw storage. It is not the common base type of vectors and does not provide runtime rank, covariance, or element-type metadata.
Ordering and search
Sort covers whole vectors, subranges, and comparison functions. Default floating-point ordering deliberately moves NaN before other values to satisfy the current .NET-oriented comparer contract and avoid passing a non-strict NaN comparator to std::sort. BinarySearch assumes compatible ordering. IndexOf, LastIndexOf, Find, FindLast, FindAll, forward/backward predicate index searches, Exists, and TrueForAll cover linear discovery.
Mutation
Reverse, Clear, Fill, and Resize mutate an owned vector. Clear assigns value-initialized elements; it does not necessarily change the vector size. Resize changes size and can relocate. ConvertAll creates a new vector. ForEach invokes a supplied action and rejects an empty callable.
Copying
Vector copy overloads require an already sized destination and validate source/destination ranges. Current copying is element-wise and overlap-aware. It preserves non-trivial element semantics, so copying std::string assigns strings instead of duplicating their internal bytes. ConstrainedCopy forwards to the same current vector behavior; there is no CLR transactional array type system behind it.
Raw-pointer copy validates signed offsets/length and relevant null pointers, then performs overlap-aware element assignment. It cannot validate capacity. Forming a valid pointer does not prove that the pointed-to range exists.
Bounds and signed arithmetic
Sharp Runtime range APIs commonly use the 32-bit signed intcs compatibility type. Current remediation avoids checks of the form start + length <= size where signed addition can overflow and bypass validation. It first validates the start against size, then compares length with size - start using unsigned-domain checks.
Direct native vector[i] remains unchecked. vector.at(i), Span indexers, and runtime helper methods provide checked alternatives with their own exception types. Do not assume a Sharp Runtime wrapper can protect an unchecked native access performed elsewhere.
Span and ReadOnlySpan
A span contains a pointer and an intcs element length. It can view a vector, raw contiguous storage, stack storage, or another slice. Indexing throws IndexOutOfRangeException; invalid slice ranges throw ArgumentOutOfRangeException. ToArray returns an owning vector.
CopyTo rejects a shorter destination. TryCopyTo returns false without writing when the destination is too short. Both handle overlapping ranges by choosing a safe direction. Fill and Clear mutate through a mutable span. A read-only span prevents mutation through that view, not mutation through another alias.
Span equality compares pointer and length—the identity of the view. Use SequenceEqual-shaped behavior or an explicit algorithm for element equality.
Memory and ReadOnlyMemory are borrowed here
The implementation stores a pointer to a vector plus offset and length. A Memory<T> does not own that vector and does not retain it through a shared owner. This differs materially from managed Memory<T>, which is designed to survive async boundaries by retaining suitable backing storage.
getSpanProperty() computes a view over the vector’s current data(). If the vector is destroyed, moved-from in a way that changes storage, or reallocated, previously obtained pointers/spans and the Memory object’s assumptions become invalid. The Memory object retaining the address of the vector object does not freeze its buffer.
Pin() does exist despite a stale introductory comment in Memory.hpp. It returns a MemoryHandle exposing the current native pointer. With no GC, “pin” does not retain the vector or prevent reallocation. The handle is only as safe as the owner’s stability protocol.
ArraySegment
An ArraySegment<T> stores a pointer to a vector, an offset, and a count. Construction validates the range without signed overflow. Indexing, slicing, copying, containment, and conversion reject a default/null segment with InvalidOperationException before touching storage.
There is one current deviation: begin() and end() remain noexcept and return null for a default segment, so a range-for performs zero iterations where the corresponding .NET enumerator would throw. This exception-specification change is unresolved at the selected pin.
ArraySegment::CopyTo can resize a shorter destination vector in the current adaptation. That differs from an already-sized .NET array boundary and should be tested rather than assumed.
Ownership, passing, and lifetime
| Function intent | Recommended parameter | Contract |
|---|---|---|
| Consumes/copies an owned sequence | std::vector<T> by value | Caller moves or copies ownership |
| Mutates an existing owned vector | std::vector<T>& | May resize/invalidate; document it |
| Reads an existing vector | const std::vector<T>& | Synchronous borrow |
| Reads any contiguous storage | ReadOnlySpan<T> | Non-owning element range |
| Mutates any contiguous storage | Span<T> | Non-owning writable range |
| Retains across async work | Explicit shared/unique owner plus offsets | Do not rely on current Memory to retain storage |
Invalidation matrix
| Owner operation | References/iterators | Span | Memory/segment |
|---|---|---|---|
| Destroy vector | Invalid | Invalid | Invalid |
| Reallocate vector | Invalid | Invalid | Object still points to vector, but existing spans/handles invalid; range must remain meaningful |
| Erase/insert | Affected and later positions can invalidate | View contents/positions may no longer mean the same thing | Stored offset/count may describe different elements or exceed size |
| Overwrite elements only | Usually structurally valid | Sees new values | Sees new values |
| Move vector object | Depends on operation and allocator | Do not rely on old owner protocol | Pointer to original vector object can become semantically stale |
Copying and overlap
Overlapping copies are easy to get wrong for non-trivial elements. A forward loop that copies a range one position to the right overwrites values it has not read yet. memcpy is invalid for overlap and unsafe for owning objects such as strings. Current Array, Span, Memory, and ArraySegment copy paths use a shared overlap-aware element operation.
The destination’s element lifetime already exists for vector/span assignment. Raw uninitialized storage requires construction-aware native algorithms instead of these assignment APIs.
Multidimensional and jagged data
The current core does not expose a general CLR rectangular array with rank, lower bounds, runtime element type, and covariance. Choose a representation explicitly:
vector<vector<T>>for jagged rows with independent allocation;- a flat
vector<T>plus checkedrow * width + columnfor rectangular dense storage; - a numeric matrix type when algebraic operations are the real abstraction;
- a domain object that validates dimensions and owns indexing policy.
Check multiplication for overflow before allocating or indexing a flat matrix. Decide row-major versus column-major layout at the public boundary.
Comparison with C# arrays and std::vector
| Property | C# array | std::vector / Sharp Runtime |
|---|---|---|
| Length | Fixed after construction | Vector size can change; Array::Resize can relocate |
| Runtime base | Derives from CLR System.Array | No Array base object |
| Bounds | Runtime checked | [] unchecked; helpers/views can check |
| Covariance | Reference arrays have runtime covariance checks | Template element types are invariant/distinct |
| Ownership | GC reference retains array | Vector value owns; views do not retain it |
| Multidimensional | Rectangular and jagged forms | Choose nested/flat/domain-specific representation |
| Element copy | Value or reference semantics under CLR | Exact C++ copy/move/assignment of T |
When to use which representation
- Use
std::vectorfor ordinary owned dynamic sequences. - Use
std::arrayfor fixed compile-time extents. - Use Span/ReadOnlySpan for synchronous generic contiguous access with explicit bounds.
- Use Array helpers when their familiar validation, sorting, copying, or search contract helps a port.
- Use List-shaped collections when enumerator/versioning and collection interfaces matter.
- Use an explicit owner—not borrowed Memory—when work crosses a lifetime or async boundary.
- Use a matrix/domain type for multidimensional invariants.
Porting checklist
- Identify whether the C# variable owns the array or only receives a view.
- Choose the element ownership model before choosing the container.
- Replace CLR bounds guarantees with checked helpers or explicit validation where needed.
- Mark every operation that can resize or relocate.
- Do not retain spans, segments, references, or pointers across those operations.
- Test empty/default views, negative indices, extreme ranges, overlap, non-trivial elements, and owner destruction.
- Document multidimensional layout and overflow policy.