Type System
Use familiar type landmarks without mistaking native C++ values, ownership, RTTI, and templates for a CLR type system.
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# name | Sharp Runtime alias | Native type and width |
|---|---|---|
sbyte / SByte | sbytecs / SByte | int8_t, 8-bit signed |
byte / Byte | bytecs / Byte | uint8_t, 8-bit unsigned |
short / Int16 | shortcs / Int16 | int16_t, 16-bit signed |
ushort / UInt16 | ushortcs / UInt16 | uint16_t, 16-bit unsigned |
int / Int32 | intcs / Int32 | int32_t, 32-bit signed |
uint / UInt32 | uintcs / UInt32 | uint32_t, 32-bit unsigned |
long / Int64 | longcs / Int64 | int64_t, 64-bit signed |
ulong / UInt64 | ulongcs / UInt64 | uint64_t, 64-bit unsigned |
float / Single | Single | float |
char | charcs | char16_t, a UTF-16 code unit type |
string | String | std::string, used as UTF-8 byte storage |
IntPtr | IntPtr | std::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.
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:
| Need | Typical C++ form | Consequence |
|---|---|---|
| Required subobject with same lifetime | Direct member | No null; enclosing owner controls destruction |
| Exclusive dynamic object | std::unique_ptr<T> | Move-only ownership; borrows invalidate on destruction |
| Genuinely shared identity | std::shared_ptr<T> | Reference count retains object; cycles need weak_ptr |
| Non-owning observer | Reference or raw pointer | Caller must prove owner outlives access |
| Optional value | Nullable<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.HasValue | score.getHasValueProperty() | Property naming follows current generated accessor conventions |
score.Value | score.getValueProperty() | Throws when absent |
score ?? 0 | score.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::variantfor a closed set of values; - a polymorphic base plus smart pointers for a deliberate hierarchy;
- a type-erased callable or
std::anywhen 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# intent | Native technique | Boundary |
|---|---|---|
x is IFoo | dynamic_cast<IFoo*>(x) != nullptr | Requires a participating polymorphic hierarchy |
x as IFoo | dynamic_cast<IFoo*>(x) | Borrowed pointer; owner remains elsewhere |
| Numeric conversion | Checked helper or validated static_cast | C++ narrowing and overflow rules differ |
| Generic conversion | Constrained template/converter | No 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 family | Usually owns? | Invalidation or lifetime concern |
|---|---|---|
| Primitive/wrapper/value | Owns its value | Copy/move and native overflow |
std::string/std::vector | Own their buffers/elements | Mutation can invalidate references and views |
Span/Memory/ArraySegment | No | Owner destruction or relocation |
Object*/interface pointer | Not by itself | Must identify a longer-lived owner |
unique_ptr | Yes, exclusively | Moves transfer ownership |
shared_ptr | Yes, jointly | Cycles and delayed cleanup |
| Delegate/event callback | Owns captures according to callable | Publisher 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.