Type System

Use familiar type landmarks without mistaking native C++ values, ownership, RTTI, and templates for a CLR type system.

Core modelOwnershipCompatibility boundaries

A native type system with compatibility landmarks

Sharp Runtime does not recreate the Common Type System. C++ remains the language and runtime: templates are instantiated at compile time, values have native layout, polymorphism is opt-in, RTTI is compiler-provided, destructors are deterministic, and there is no managed heap. The library supplies familiar aliases, wrappers, interfaces, exceptions, and helper algorithms where they make source ports clearer.

The most important design question is therefore not “what is the C++ spelling of this C# type?” but “is this concept a value, an owner, a shared identity, or a borrow in this port?” The spelling follows that decision.

Primitive aliases

SharpRuntime/SharpRuntimeHelper.hpp defines exact-width aliases used throughout the library and by ports. They are type aliases, not boxed runtime objects.

C# nameSharp Runtime aliasNative type and width
sbyte / SBytesbytecs / SByteint8_t, 8-bit signed
byte / Bytebytecs / Byteuint8_t, 8-bit unsigned
short / Int16shortcs / Int16int16_t, 16-bit signed
ushort / UInt16ushortcs / UInt16uint16_t, 16-bit unsigned
int / Int32intcs / Int32int32_t, 32-bit signed
uint / UInt32uintcs / UInt32uint32_t, 32-bit unsigned
long / Int64longcs / Int64int64_t, 64-bit signed
ulong / UInt64ulongcs / UInt64uint64_t, 64-bit unsigned
float / SingleSinglefloat
charcharcschar16_t, a UTF-16 code unit type
stringStringstd::string, used as UTF-8 byte storage
IntPtrIntPtrstd::uintptr_t, unsigned pointer-sized integer

The aliases reproduce useful widths and names. They do not import checked arithmetic, CLR numeric conversions, boxing identity, or metadata. Signed overflow still follows C++ rules unless a checked wrapper or algorithm explicitly intercepts it.

Two different character units

charcs is a 16-bit code unit, while the common String representation stores UTF-8 bytes. Do not index an std::string with a UTF-16 offset or assume one char is one user-visible character.

Value-like types

Primitive aliases, numeric wrappers, DateTime-shaped structures, Guid, Version, Nullable<T>, vectors and matrices, strings, and many option/result types are ordinary C++ values. Construction, assignment, copying, moving, destruction, alignment, and container placement follow their C++ definitions.

A value member is usually the safest translation for a required C# value type. It cannot be null, it shares the enclosing object’s lifetime, and its destructor runs deterministically. But copying a native value may duplicate state where managed code copied only a reference. Review each port for identity rather than classifying by name alone.

Reference-like and polymorphic types

Some hierarchies participate in System::Object, some implement native abstract interfaces, and some APIs exchange pointers or smart pointers. C++ has no universal reference-type category. The port must choose an owner:

NeedTypical C++ formConsequence
Required subobject with same lifetimeDirect memberNo null; enclosing owner controls destruction
Exclusive dynamic objectstd::unique_ptr<T>Move-only ownership; borrows invalidate on destruction
Genuinely shared identitystd::shared_ptr<T>Reference count retains object; cycles need weak_ptr
Non-owning observerReference or raw pointerCaller must prove owner outlives access
Optional valueNullable<T> or std::optional<T>Presence is distinct from pointer identity

System::Object: an opt-in base

System::Object is an abstract polymorphic base for participating Sharp Runtime hierarchies. Its virtual destructor allows destruction through an Object*. Default Equals uses pointer identity, static ReferenceEquals compares pointers, and default GetHashCode derives a non-negative value from the address. A derived value-equality class must override equality and hashing together.

ToString defaults to the project-specific GetTypeName. Concrete derived classes supply a stable type-name string, commonly through declaration/definition macros. GetType uses typeid(*this) to observe the dynamic C++ type.

None of this makes Object a universal root. std::string, std::vector, primitives, templates, and unrelated native classes do not acquire an object header or become assignable to Object*. Code that accepts arbitrary managed object must be redesigned around a constrained variant, template, interface, or explicitly owned hierarchy.

System::Type and the reflection boundary

System::Type wraps a pointer to std::type_info. Type::From<T>() represents a compile-time C++ type, while Object::GetType() wraps dynamic RTTI. Equality compares RTTI identity and hashing uses type_info::hash_code.

This is type identity, not CLR reflection. Names come from type_info::name() and may be mangled. Name and FullName return the same string. The current IsClass, IsValueType, IsAbstract, IsSealed, and IsInterface properties are fixed compatibility stubs; they are not mutually dependable classifications. There is no member enumeration, custom-attribute discovery, generic metadata, dynamic construction table, or method invocation engine.

const System::Type key = System::Type::From<MyService>();

// Appropriate: an RTTI-backed key or identity comparison.
services.emplace(key, service);

// Inappropriate: branching on getIsValueTypeProperty() as CLR truth.

Strings

SharpRuntime::String is an alias for std::string. It owns its byte buffer, copies and moves as a native value, can be mutated, and cannot represent managed null by itself. System::String is a deleted-constructor static helper class over that representation; it is not a wrapper instance.

Most indices and lengths are UTF-8 byte units. Substring returns a copy, not a shared backing view. Comparisons are byte-oriented, and the six StringComparison names collapse largely to case-sensitive versus per-byte case-insensitive behavior; they do not provide complete current/invariant/ordinal culture engines. Hashes use native std::hash<std::string> and are not stable serialized identifiers or .NET-compatible hash codes.

Arrays and owned sequences

The common one-dimensional owned sequence is std::vector<T>. System::Array is a static algorithm class operating primarily on vectors and some raw buffers. It offers sorting, copying, clearing, searching, reversing, resizing, and related helpers; it is not a base object for every array.

A vector owns elements and may reallocate when size or capacity changes. Reallocation invalidates pointers, references, iterators, spans, memory views, and segments into its old storage. Copying a vector copies elements according to T; copying a vector of shared_ptr shares pointees, while copying values duplicates them. Raw-buffer overloads cannot validate capacity because pointers carry no length; callers must supply valid storage.

There is no general CLR rectangular multidimensional array model in the current core. Use nested vectors for jagged structure, a checked flattened index for rectangular data, or a domain-specific matrix type where appropriate. Document layout and bounds rather than assuming C# array rank behavior.

Span, ReadOnlySpan, Memory, and ArraySegment

Span<T> and ReadOnlySpan<T> are pointer-plus-length views over contiguous native elements. They bounds-check indexing and slicing, and their copy operations are overlap-safe. Equality describes view identity—pointer and length—not sequence content. They never own or extend the source lifetime.

Memory<T> and ReadOnlyMemory<T> are also non-owning in this implementation: they retain a pointer to a vector plus offset and length. This is a major difference from managed Memory<T>, which can retain its backing owner. The current Pin exposes a pointer in a MemoryHandle; it does not prevent a native vector from reallocating and does not retain that vector.

ArraySegment<T> is a borrowed vector range. A default segment is empty/null-shaped and several property or operation accesses reject it, while iteration over its null begin/end behaves as an empty range. Do not retain any of these views across vector mutation unless the operation is proven not to relocate storage.

Nullable values

System::Nullable<T> wraps std::optional<T>. A default instance has no value; construction from T stores a value. HasValue reports presence, Value returns the contained value or throws InvalidOperationException when absent, and GetValueOrDefault returns either the value or an established default.

C#Sharp Runtime / C++Porting note
int? score = null;System::Nullable<int> score;No heap allocation or object boxing
score.HasValuescore.getHasValueProperty()Property naming follows current generated accessor conventions
score.Valuescore.getValueProperty()Throws when absent
score ?? 0score.GetValueOrDefault()Or branch explicitly when fallback is not T{}

The contained type controls copy, move, destruction, and resource semantics. A nullable unique_ptr usually adds little; the pointer is already nullable. A nullable value is different from a borrowed pointer that can dangle.

Enums

C++ enum class remains the primary representation for closed named integral choices. Sharp Runtime supplies enum-shaped helpers and numerous familiar enum declarations, but there is no CLR metadata table that automatically maps every value to attributes or names. Numeric conversion, parsing, validation, and flags behavior exist only where the concrete enum/helper implements them.

Use explicit casts at interop boundaries and validate unknown integral values when the upstream protocol can evolve. Do not assume that a value lying within the underlying integer range is a declared enum member.

Boxing and unboxing

There is no general boxing operation that turns any C++ value into a managed Object. The current RuntimeHelpers::Box path throws PlatformNotSupportedException. Consequently, APIs that rely on arbitrary object payloads need an explicit native representation such as:

  • a constrained std::variant for a closed set of values;
  • a polymorphic base plus smart pointers for a deliberate hierarchy;
  • a type-erased callable or std::any when its reduced guarantees are acceptable;
  • a template when the type is known at compile time.

Each alternative has different lifetime, conversion, and failure behavior. Hiding the choice behind a fake boxing label makes a port harder to reason about.

Casts and type tests

Use C++ casts according to the representation. dynamic_cast is appropriate for a polymorphic hierarchy with RTTI; static_cast is appropriate only when the relationship is established; numeric casts may narrow and should be validated or replaced by checked helpers; pointer casts do not establish ownership.

C# intentNative techniqueBoundary
x is IFoodynamic_cast<IFoo*>(x) != nullptrRequires a participating polymorphic hierarchy
x as IFoodynamic_cast<IFoo*>(x)Borrowed pointer; owner remains elsewhere
Numeric conversionChecked helper or validated static_castC++ narrowing and overflow rules differ
Generic conversionConstrained template/converterNo universal CLR conversion service

Equality and hashing

Equality is type-specific. Native values usually use their operators. Object defaults to identity. Collection comparers may impose .NET-oriented policies—for example, current array sorting treats floating-point NaN ordering deliberately rather than passing a non-strict comparator to std::sort. String helper equality is byte/case-policy based.

The invariant is familiar but still manual: if a type supplies value equality, equal values must produce equal hashes under the comparer used by a dictionary or set. Address-derived object hashes are only suitable for identity equality. Standard-library hash results and RTTI hashes should not be persisted or compared with .NET outputs.

Conversions and formatting

Wrapper types expose parsing and conversion surfaces where implemented. They may translate established overflow, format, and argument errors into Sharp Runtime exceptions. That does not create CLR-wide IConvertible semantics. Culture inputs, floating special values, integer widths, and native compiler capabilities remain part of each contract.

Composite string formatting supports a bounded grammar and a bounded set of argument behavior, using native/classic-locale machinery. It is not a general reflection-based formatter. Encoding conversions belong to System::Text; they should not be replaced with byte reinterpretation when source and destination encodings differ.

Interfaces

Interfaces are expressed as C++ abstract base classes or templates. They provide compile-time names and virtual contracts, not CLR interface maps. Use virtual destructors when deleting through an interface pointer. Decide whether an API borrows an interface, uniquely owns it, or shares it; the interface declaration itself does not answer that question.

Default interface methods, variance, runtime interface enumeration, and arbitrary managed casts do not appear automatically. Some familiar generic variance can be represented through templates, but each C++ instantiation remains a distinct type.

Exceptions are types, not a CLR

The runtime defines a broad hierarchy of familiar exception names. They are thrown as native C++ objects, caught by reference, and unwind destructors. Causal exceptions may be retained through the implemented pointer fields; native system errors are translated at component boundaries. Exact message text, stack traces, filters, serialization, and managed dispatch behavior are not universal.

try {
    const auto value = nullable.getValueProperty();
    use(value);
} catch (const System::InvalidOperationException& error) {
    log(error.what());
}

Ownership consequences at a glance

Type familyUsually owns?Invalidation or lifetime concern
Primitive/wrapper/valueOwns its valueCopy/move and native overflow
std::string/std::vectorOwn their buffers/elementsMutation can invalidate references and views
Span/Memory/ArraySegmentNoOwner destruction or relocation
Object*/interface pointerNot by itselfMust identify a longer-lived owner
unique_ptrYes, exclusivelyMoves transfer ownership
shared_ptrYes, jointlyCycles and delayed cleanup
Delegate/event callbackOwns captures according to callablePublisher may outlive captured objects

What has no native CLR equivalent

Compiler templates, deterministic destructors, references, move-only ownership, allocator behavior, iterator invalidation, object layout, undefined behavior, and ABI compatibility have no direct CLR equivalents. Conversely, garbage-collector reachability, universal boxing, runtime generic metadata, managed array covariance, reflection invocation, and assembly loading have no direct Sharp Runtime equivalents.

A high-quality port acknowledges both directions. Use Sharp Runtime where a familiar API and tested behavior reduce translation risk; use idiomatic C++ where emulating a managed-only mechanism would hide lifetime or type constraints.