System::String Helpers
A practical behavioral reference for the static String algorithms over Sharp Runtime’s native UTF-8 std::string representation.
What System::String is
System::String is a non-instantiable class of static algorithms over std::string. The actual common string value is SharpRuntime::String, an alias for std::string. A string therefore owns a native byte buffer, copies and moves as a value, can be modified through standard C++ operations, and has no CLR object header.
#include <System/String.hpp>
using System::String;
const std::string raw = " alpha,beta,gamma ";
const std::string clean = String::Trim(raw);
const auto parts = String::Split(clean, ',');
const std::string label = String::Join(" | ", parts);
The example uses the static helpers without constructing a System::String. Select Core.Base and link SharpRuntime::Core.Base; no separate text component is needed for these foundational operations.
Construction and native values
Construct, copy, move, reserve, and mutate the underlying value exactly as an std::string. System::String::Empty supplies an empty constant, while Create(count, ch) constructs repeated bytes. The helper has no constructor because it stores no string instance.
| Intent | Native / Sharp Runtime form | Meaning |
|---|---|---|
| Empty value | std::string value; or String::Empty | An owned zero-byte string, not null |
| Literal | std::string value = "text"; | Copies literal bytes |
| Repeated byte | String::Create(8, '-') | Eight copies of one char |
| Preallocate | value.reserve(bytes); | Native capacity optimization |
| Optional string | std::optional<std::string> or API-specific pointer | Presence is explicit; empty is not substituted for null |
A reference parameter such as const std::string& cannot be null. That does not mean every C# null should silently become empty. Preserve absence with an optional or pointer when it has distinct meaning.
Encoding and index units
The runtime treats ordinary strings as UTF-8 byte storage. The helper delegates much of its work to std::string, so lengths, positions, counts, ranges, and returned indices are bytes. .NET System.String uses UTF-16 code units. Neither unit is the same as a Unicode scalar or a user-perceived grapheme.
| Text | UTF-8 bytes here | UTF-16 units in .NET | Graphemes |
|---|---|---|---|
A | 1 | 1 | 1 |
é precomposed | 2 | 1 | 1 |
| 😀 | 4 | 2 | 1 |
e + combining acute | 3 | 2 | 1 |
Substring, IndexOf, LastIndexOf, Remove, Insert, ToCharArray, padding widths, and range-taking overloads all operate on this native byte representation. A valid byte range can still cut through a multi-byte scalar. Use an encoding/scalar-aware layer when a port stores human-character offsets.
Empty and whitespace tests
IsEmpty and IsNullOrEmpty both test whether the supplied reference is empty; the parameter itself cannot be null. IsNullOrWhiteSpace scans bytes with the native character-classification function. It correctly recognizes the implemented byte/locale whitespace set but is not .NET Unicode whitespace classification.
if (System::String::IsNullOrWhiteSpace(input)) {
throw System::ArgumentException("A non-blank name is required.");
}
Comparison modes
The API exposes the six familiar StringComparison names. The current implementation reduces them to two paths: the three IgnoreCase values lowercase each byte before comparing; the other three use case-sensitive byte comparison. It does not implement distinct current-culture, invariant-culture, and ordinal collation engines.
| Requested mode | Current implementation | Important reduction |
|---|---|---|
Ordinal, CurrentCulture, InvariantCulture | Case-sensitive byte comparison | Culture names do not select different collation |
The three *IgnoreCase values | Per-byte std::tolower, then byte comparison | Not Unicode case folding; result can depend on C locale |
CompareOrdinal | Byte-by-byte ordering | UTF-8 byte ordering differs from UTF-16 ordering for some text |
Compare returns negative, zero, or positive. Equals, StartsWith, EndsWith, Contains, IndexOf, and LastIndexOf offer comparison-aware overloads. Use the case-sensitive path for protocol tokens only when that protocol defines byte comparison; do not label the current ignore-case path as Unicode invariant.
Search and range validation
Search families cover strings, bytes, start positions, counts, comparison modes, and “any of” sets. A missing value returns -1. Range-taking overloads validate signed inputs and reject invalid positions with Sharp Runtime argument exceptions. The current post-audit implementation bounds backward searches instead of allowing a negative or oversized position to wrap into size_t.
IndexOfsearches forward and returns a byte offset.LastIndexOfsearches backward and returns a byte offset.IndexOfAnyandLastIndexOfAnycompare individualcharbytes.Contains,StartsWith, andEndsWithavoid exposing a numeric position.
If a search result will later index another representation—UTF-16 text, code points, a rendered label, or a different normalization form—convert the unit explicitly.
Substring, insertion, removal, and replacement
Substring returns a new owning std::string; it does not retain a view into the source. Insert, Remove, and both Replace families also return new strings, leaving their input unchanged. This helper-level immutability is convenient for ports even though callers can still mutate the underlying std::string directly.
const std::string version = "v1.2.3";
const std::string numbers = String::Substring(version, 1);
const std::string major = String::Substring(numbers, 0, 1);
const std::string masked = String::Replace(version, '.', '-');
The integer arguments are byte positions and lengths. Replacing a string walks non-overlapping matches and returns the original value when oldValue is empty. Character replacement compares single bytes.
Split and join
Split supports a single byte delimiter, a set of delimiter bytes, or a string delimiter. Overloads accept StringSplitOptions for the implemented removal/trimming policy. Join supports string or byte separators and collections of strings, 32-bit integers, and doubles; an initializer-list string overload is also available.
Delimiter semantics follow bytes unless a multi-byte delimiter is supplied as a whole string. A set of char delimiters is a set of bytes, not Unicode scalars. When parsing a textual protocol, first decide whether separators are ASCII protocol bytes or user-language characters.
const auto fields = String::Split("alpha::beta::gamma", "::");
const std::string csv = String::Join(',', fields);
Trim, padding, and case conversion
The parameterless trim methods recognize the ASCII whitespace bytes space, tab, line feed, carriage return, form feed, and vertical tab. Custom trim sets are vectors of bytes. Padding returns a new byte string with spaces or a supplied byte until the requested byte width is reached.
ToUpper and ToLower apply the native C character conversion to each unsigned byte. The invariant-named helpers forward to the same basic behavior. This is suitable for ASCII-oriented identifiers and deliberately insufficient for general Unicode casing, expansions, Turkish-I behavior, or culture-aware display text.
Concatenation
Concat supplies two-, three-, four-, and vector-of-string overloads. The vector implementation precomputes byte size and reserves once. Ordinary operator+, append, and StringBuilder remain valid native choices; choose based on clarity and allocation behavior rather than on managed syntax alone.
Composite formatting
Format supports a finite set of overloads for strings, characters, booleans, 32/64-bit integers, floats, and doubles, with selected one-, two-, and three-argument combinations. The parser handles indexed placeholders and a bounded format-specifier grammar. Numeric formatting uses native/classic-locale machinery.
const std::string status = String::Format(
"Processed {0} items in {1} ms", 42, 7);
Do not infer arbitrary CLR IFormattable, custom formatter providers, reflection over object arguments, or every .NET numeric/date specifier. If the exact overload or format is not present in the pinned header, prefer an explicit native formatter or add a tested runtime feature.
Conversions
ToString(int, width, fill) formats a 32-bit integer with a native stream width and fill. Other numeric wrappers expose their own parse/format methods. Encoding conversions belong to System::Text::Encoding: GetBytes produces a byte vector and GetString decodes input according to the selected encoding.
Do not treat a byte reinterpretation as a conversion. UTF-8, ASCII, Latin-1, UTF-16 little/big endian, UTF-32, and UTF-7 are distinct concrete surfaces. The current Encoding count APIs report UTF-8 storage-byte units, and shared factory instances have mutable fallback properties; those are documented limitations rather than CLR parity.
ToCharArray and charcs
ToCharArray returns std::vector<char> containing UTF-8 storage bytes. It does not return std::vector<charcs> of UTF-16 units. SharpRuntime::charcs exists for APIs that explicitly model a 16-bit code unit, but it does not change the string storage type.
For non-ASCII input, ToCharArray is not the direct equivalent of C# string.ToCharArray(). Use the encoding or Unicode helper that matches the unit your algorithm needs.
Equality and hashing
Default helper equality compares bytes. Comparison-mode equality follows the two-path reduction described above. GetHashCode folds a native std::hash<std::string> result into a 32-bit signed value; the widening step avoids invalid shifts on 32-bit platforms.
The hash is intended for an in-process native hash container under matching equality. It is not stable across standard-library implementations or processes, not suitable for persisted data or network protocols, and not promised to equal a .NET string hash.
Interning and identity
Intern and IsInterned currently return their input value. There is no intern pool and no reference identity for ordinary strings. Comparing addresses or c_str() pointers is never a substitute for value equality.
Important differences from .NET
| .NET String | Sharp Runtime | Porting consequence |
|---|---|---|
| UTF-16 code-unit storage | UTF-8 bytes in std::string | Indices, lengths, ordering, and arrays differ |
| Immutable managed object | Mutable native value; helpers return copies | Native mutation and invalidation are visible |
| Reference can be null | std::string value cannot be null | Use optional/pointer when absence matters |
| Rich culture/ordinal comparison engines | Byte comparison plus bytewise ignore-case reduction | Do not promise Unicode/culture parity |
| Unicode normalization | Current extension returns input and reports true | Use a real Unicode library when normalization matters |
| Runtime interning | No intern pool | Only value comparison is meaningful |
| Stable contract within CLR version | Native std::hash folded to 32 bits | Never persist the hash |
When to use another representation
- Use
std::string_viewfor a short synchronous byte-string borrow when the owner is clear. - Use byte spans for binary data; do not label arbitrary bytes as text.
- Use
charcsor an explicit UTF-16 container only at a boundary that requires UTF-16. - Use a Unicode library for normalization, grapheme segmentation, language-sensitive casing, or collation.
- Use
StringBuilderfor repeated append/format operations where its byte-unit mutation model is acceptable.