Array Internals

Understand the storage, range validation, copying, invalidation, and lifetime contracts behind every Sharp Runtime array-shaped representation.

OwnershipBoundsViews and invalidation

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.

RepresentationOwns elements/storage?Can resize?Primary use
Native T[N] / std::array<T,N>Yes, inlineNoCompile-time fixed native shape
std::vector<T>YesYesDefault owned one-dimensional sequence
System::ArrayNo instanceAlgorithms can resize a vectorFamiliar sort/copy/search/clear behavior
Span<T>NoNoMutable pointer-and-length borrow
ReadOnlySpan<T>NoNoRead-only contiguous borrow
Memory<T>/ReadOnlyMemory<T>No in this implementationNoBorrowed vector region that can produce a span
ArraySegment<T>NoNoOffset/count view into a vector
List<T>-shaped collectionYesYesCollection 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.

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 intentRecommended parameterContract
Consumes/copies an owned sequencestd::vector<T> by valueCaller moves or copies ownership
Mutates an existing owned vectorstd::vector<T>&May resize/invalidate; document it
Reads an existing vectorconst std::vector<T>&Synchronous borrow
Reads any contiguous storageReadOnlySpan<T>Non-owning element range
Mutates any contiguous storageSpan<T>Non-owning writable range
Retains across async workExplicit shared/unique owner plus offsetsDo not rely on current Memory to retain storage

Invalidation matrix

Owner operationReferences/iteratorsSpanMemory/segment
Destroy vectorInvalidInvalidInvalid
Reallocate vectorInvalidInvalidObject still points to vector, but existing spans/handles invalid; range must remain meaningful
Erase/insertAffected and later positions can invalidateView contents/positions may no longer mean the same thingStored offset/count may describe different elements or exceed size
Overwrite elements onlyUsually structurally validSees new valuesSees new values
Move vector objectDepends on operation and allocatorDo not rely on old owner protocolPointer 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 checked row * width + column for 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

PropertyC# arraystd::vector / Sharp Runtime
LengthFixed after constructionVector size can change; Array::Resize can relocate
Runtime baseDerives from CLR System.ArrayNo Array base object
BoundsRuntime checked[] unchecked; helpers/views can check
CovarianceReference arrays have runtime covariance checksTemplate element types are invariant/distinct
OwnershipGC reference retains arrayVector value owns; views do not retain it
MultidimensionalRectangular and jagged formsChoose nested/flat/domain-specific representation
Element copyValue or reference semantics under CLRExact C++ copy/move/assignment of T

When to use which representation

  • Use std::vector for ordinary owned dynamic sequences.
  • Use std::array for 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

  1. Identify whether the C# variable owns the array or only receives a view.
  2. Choose the element ownership model before choosing the container.
  3. Replace CLR bounds guarantees with checked helpers or explicit validation where needed.
  4. Mark every operation that can resize or relocate.
  5. Do not retain spans, segments, references, or pointers across those operations.
  6. Test empty/default views, negative indices, extreme ranges, overlap, non-trivial elements, and owner destruction.
  7. Document multidimensional layout and overflow policy.