System::Nullable

Represent optional values with native construction and equality rules, without confusing absence with pointer ownership.

Core.Basestd::optional-backedSource-verified

Role and storage

System::Nullable<T> represents either no value or one value of T. It is an ordinary header-only C++ template backed by a private std::optional<T>; it is not a boxed object, does not derive from System::Object, and does not introduce CLR nullable metadata.

The type lives in System/Nullable.hpp and belongs to Core.Base. A default-constructed instance is absent. Constructors from const T& and T&& are intentionally implicit, mirroring the convenient C# conversion from T to T?.

#include <System/Nullable.hpp>

System::Nullable<int> missing;
System::Nullable<int> answer = 42;

if (answer.getHasValueProperty()) {
    const int value = answer.getValueProperty();
    (void)value;
}

Public surface

MemberBehaviorImportant detail
getHasValueProperty()Reports presencenoexcept; equivalent to the backing optional's has_value()
getValueProperty()Returns the contained valueReturns const T&; throws when absent
GetValueOrDefault()Returns the value or T{}Returns by value
GetValueOrDefault(defaultValue)Returns the value or a supplied fallbackReturns by value; the fallback is passed as const T&
Equals / GetHashCodeDefault-equality and matching hash contractDifferent from lifted operator== for floating NaN
ToStringEmpty text when absent; formatted contained value when presentPrefers T::ToString(), otherwise stream insertion
explicit operator boolTests presenceA C++ convenience, not nullable-Boolean value conversion
explicit operator TExtracts the valueUses the same throwing access as Value
operator== / operator!=Compares two nullable values, or presence against std::nulloptUses the backing optional's lifted underlying operator

Value access and exceptions

getValueProperty() throws System::InvalidOperationException with the message “Nullable object must have a value.” when absent. Explicit conversion to T delegates to that accessor and throws the same exception. The returned property reference remains valid only while the Nullable object exists and is not assigned a replacement value.

GetValueOrDefault() never throws merely because the nullable is absent. The parameterless overload value-initializes T; that means zero for arithmetic values, but it is not necessarily the correct domain fallback. Prefer the explicit-default overload when zero, an empty string, or a default-constructed object would hide missing input.

System::Nullable<int> port;

const int conventional = port.GetValueOrDefault();    // 0
const int configured = port.GetValueOrDefault(8080);  // 8080

if (!port) {
    // Presence test only; no value was extracted.
}

Equality, ordering, and floating-point behavior

Equals treats two absent values as equal, an absent and present value as unequal, and two present values through Sharp Runtime's default equality policy. For floating types that policy follows .NET's Single.Equals/Double.Equals shape: two NaNs compare equal and signed zero values compare equal.

operator== deliberately has a different floating behavior. It models lifted C# == by using the underlying equality operator through std::optional. Two present NaNs therefore compare false, just as two nullable NaNs do under lifted == in C#. This is not an inconsistency to “fix”; it represents two distinct public equality surfaces.

OperandsEqualsoperator==
absent, absenttruetrue
absent, presentfalsefalse
equal ordinary valuestruetrue
NaN, NaNtruefalse
+0.0, -0.0truetrue

NullableHelper::Compare orders absence before presence and compares two present values with the runtime's default comparison policy; floating NaN consequently orders before every present numeric value. NullableHelper::Equals delegates to the Equals surface. The helper has a different name because C++ cannot define a non-template Nullable beside Nullable<T> in the same namespace.

Hashing

An absent nullable hashes to zero. A present value uses the runtime's default hash policy. For floating values, NaN payloads are canonicalized so values considered equal by Equals receive the same hash; positive and negative zero also share a hash. Zero is a valid hash for a present value, so a collision with the absent state does not imply equality.

GetHashCode() returns the project-wide 32-bit intcs hash shape. It is suitable only where the consuming comparer uses the matching equality contract. Do not persist it, treat it as identity, or assume it equals a .NET process's hash.

Text conversion

ToString() returns an empty std::string for an absent value, matching the intended nullable display shape rather than claiming that absence and empty text are generally equivalent. For a present T, compile-time detection prefers a const T::ToString() convertible to std::string. Otherwise the implementation uses operator<< on a classic-locale std::ostringstream.

There is no format-provider overload, nullable format string, or reflection-based conversion. A type with neither a suitable member nor stream insertion can still be stored, but calling ToString() for that instantiation will not compile.

Ownership and template requirements

The contained value is stored inline according to std::optional's object model. Copying or moving a Nullable<T> copies or moves T; destroying it destroys a present T. It does not allocate merely to represent presence and does not extend the lifetime of an external object unless T itself is an owning smart pointer.

The template does not restrict T to CLR-style value types. That flexibility does not make every member available for every C++ type. The value-or-default and explicit-value operations return by value and therefore require the corresponding construction. Equality, hashing, and text conversion require the relevant operators or customization. The current equality and hash members are declared noexcept; a user-defined equality or hash that throws is not a safe instantiation for those calls.

Nullable is not an ownership policy

Use Nullable<T> for optional value semantics. Use unique_ptr, shared_ptr, weak_ptr, or a documented raw pointer when the problem is object ownership, shared lifetime, expiry, or borrowing. Wrapping a pointer in Nullable usually adds a second absence mechanism rather than clarity.

C# comparison

C# / .NETSharp Runtime / C++Porting consequence
T? language syntaxSystem::Nullable<T>Include the header and name the template explicitly
.HasValue / .ValuegetHasValueProperty() / getValueProperty()Property syntax becomes accessor calls
Nullable value returned by ValueConst reference returned by the property accessorDo not retain the reference beyond the nullable object's lifetime
Compiler lifts many operators over T?Only the documented equality/presence operators are supplied hereWrite explicit presence logic for arithmetic, relational, and custom operators
Boxing and Nullable.GetUnderlyingTypeNo boxing service or CLR metadataUse templates and explicit registries/type traits
null literalDefault construction; comparisons with std::nulloptThere is no general managed-null conversion

Usage pattern

#include <System/Nullable.hpp>

System::Nullable<int> parsePort(const std::string& text)
{
    if (text.empty()) return {};
    return std::stoi(text);
}

const auto port = parsePort("443");
if (port.getHasValueProperty()) {
    connectTo(port.getValueProperty());
}

This example uses absence for empty input and extracts only after testing presence. std::stoi still has native exception behavior; Nullable does not translate parsing failures automatically.