Using Strings
String is the type you can comfortably use at almost every ordinary text boundary.
It behaves like a read-only value, yet it owns or safely shares the storage that keeps its characters alive.
You can therefore pass a temporary literal, retain a slice, or copy a stored value without having to coordinate a
separate view lifetime.
This page develops that everyday model from the caller’s perspective. It explains how to accept text without needless overloads, how copies and returned values remain efficient, and when a shared slice should be turned into independent storage.
Read-Only String Parameters
A function that only reads UTF-8 text does not need to know whether the caller has a literal, a stored value, or a local
editor.
A const String & parameter gives all three a common boundary: literal data can remain static, existing strings can
share their backing store, and an editor can expose its current value safely.
One signature is therefore enough without introducing a borrowed view lifetime or a family of convenience overloads.
text/String/IdealFunctionParameter.cpp
/// `String` is the preferred parameter type for functions that read text.
///
/// It accepts common string inputs naturally:
/// - string literals are used directly without copying,
/// - existing strings share their storage,
/// - local editors convert safely to read-only strings.
///
/// From the caller's perspective, all variants behave the same.
void idealFunctionParameter() {
// A string literal can be passed directly.
countEmojis("🌲🌲 Waldkonzert mit Fuchs 🦊 und Eule 🦉"_el);
// An existing string can be passed without copying.
const auto string = el::String{"Pluie douce sur les fleurs 🌧️🌷🌼"_el};
countEmojis(string);
// A short value assembled with an editor is accepted as read-only input.
auto constructed = el::StringEditor{"Bosque nocturno: "_el};
constructed.append("luna 🌙 y grillos 🦗"_el);
countEmojis(constructed);
}
/// Count all emoji-like symbols in `text` and print the result.
///
/// The function only needs read-only access to the text. It does not need to know
/// whether the caller passed a literal, a string, or a short constructed value.
void countEmojis(const el::String &text) {
std::size_t emojiCount = 0;
text.forEach([&](const el::Char character) mutable noexcept -> el::util::LoopStatus {
if (character.isCategory(el::UnicodeCategory::OtherSymbol)) {
++emojiCount;
}
return el::LoopStatus::Continue;
});
el::io::printLine("Text ..........: \""_el, text, "\""_el);
el::io::printLine("Emoji symbols .: ", emojiCount);
el::io::printLine();
}
Output
Text ..........: "🌲🌲 Waldkonzert mit Fuchs 🦊 und Eule 🦉"
Emoji symbols .: 4
Text ..........: "Pluie douce sur les fleurs 🌧️🌷🌼"
Emoji symbols .: 3
Text ..........: "Bosque nocturno: luna 🌙 y grillos 🦗"
Emoji symbols .: 2
Storing and Returning String Values
Stored text is usually read far more often than it is changed, which makes String the honest member type.
Copying it normally copies a small handle and shares the backing store, while returning it by value benefits from normal
move and return-value optimization.
Keeping a StringEditor in a data structure merely because a future change is possible weakens that intent and makes
accidental mutation easier.
Create the editor at the moment a multi-step edit actually begins, and turn the completed result back into a String
for long-lived storage.
text/String/ConstructionAndStorage.cpp
/// Get the first word in the given text with letters.
auto getFirstWordRange(const el::String &text) -> el::ByteRange {
static const auto separator = el::CharSet::from(el::UnicodeCategoryGroup::Letter);
const auto begin = text.findFirstOf(separator);
if (begin.isNoIndex()) {
return {};
}
auto end = text.findFirstNotOf(separator, begin);
if (end.isNoIndex()) {
end = text.indexAt(el::StringSide::Back);
}
return el::ByteRange{begin, end};
}
/// This demo demonstrates how a `String` can be constructed from various sources and how it behaves.
void constructionAndStorage() {
// Constructing a `String` will just store a safe reference to the string literal.
const auto textForRead = el::String{"🌲 Schwarzwald: kühle Morgenluft"_el};
el::io::printLine("A string from a string literal: "_el, textForRead);
printMemoryAndRangeInfo(textForRead);
// A local editor is appropriate when construction and in-place editing are explicit parts of the workflow.
auto constructedText = el::StringEditor{"🌴 Bali: "_el};
constructedText.append("hangatnya angin fajar"_el);
el::io::printLine("A simply constructed string: "_el, constructedText);
printMemoryAndRangeInfo(constructedText);
// `String`s are created implicitly from `StringEditor` objects.
// The read-only string stores an owning reference to the editor's storage.
// Even when the editor is destroyed, the string and the storage remain valid.
const el::String stringFromEditor = constructedText;
// `String`s are also created implicitly from `StringLiteral` objects.
// That makes them the primary choice for function parameters and to store strings.
const el::String stringFromLiteral = "🌳 Forêt humide après la pluie"_el;
// The strings look identical but carry different storage references.
// From a user perspective, there is no difference in behavior.
el::io::printLine("String from editor : "_el, stringFromEditor);
printMemoryAndRangeInfo(stringFromEditor);
el::io::printLine("String from literal : "_el, stringFromLiteral);
printMemoryAndRangeInfo(stringFromLiteral);
// When sliced, strings keep the same storage reference but change their visible range.
auto word = stringFromEditor.slice(getFirstWordRange(stringFromEditor));
el::io::printLine("First word #1 : "_el, word);
printMemoryAndRangeInfo(word);
word = stringFromLiteral.slice(getFirstWordRange(stringFromLiteral));
el::io::printLine("First word #2 : "_el, word);
printMemoryAndRangeInfo(word);
}
Output
A string from a string literal: 🌲 Schwarzwald: kühle Morgenluft
U8String:
storageKind: literal
backingStorageId: 0x26235705d4a4d098:0x1a0ad477284abfd6
selectedRange: index: 0 - 35 (length: 35)
A simply constructed string: 🌴 Bali: hangatnya angin fajar
U8String:
storageKind: shared
backingStorageId: 0x26235778b0fc595e:0x1a0ad40a4c12361e
selectedRange: index: 0 - 32 (length: 32)
String from editor : 🌴 Bali: hangatnya angin fajar
U8String:
storageKind: shared
backingStorageId: 0x26235778b0fc595e:0x1a0ad40a4c12361e
selectedRange: index: 0 - 32 (length: 32)
String from literal : 🌳 Forêt humide après la pluie
U8String:
storageKind: literal
backingStorageId: 0x26235705d4a4d315:0x1a0ad477284abc52
selectedRange: index: 0 - 34 (length: 34)
First word #1 : Bali
U8String:
storageKind: shared
backingStorageId: 0x26235778b0fc595e:0x1a0ad40a4c12361e
selectedRange: index: 5 - 9 (length: 4)
First word #2 : Forêt
U8String:
storageKind: literal
backingStorageId: 0x26235705d4a4d315:0x1a0ad477284abc52
selectedRange: index: 5 - 11 (length: 6)
String Literals and StringLiteral
The _el suffix creates an Erbsland Core string literal with static lifetime.
Passing it directly avoids allocating and copying character data.
Construct a String from the literal when the value must be stored in a uniform container or returned through a
String interface.
text/String/EfficientLiteralUsage.cpp
/// `String` only copies a string when there is no alternative.
///
/// - By default, it keeps an owning reference to the original string and stores a range into that string.
/// - For string literals in read-only memory, where this is safe, it simply references the literal data.
///
/// This demo splices individual words from a long string literal and sorts them by character length.
/// The debug output shows that the word views select different ranges in the same backing literal storage.
///
void efficientLiteralUsage() {
// Prepare a set containing all separators used in the text.
static auto separatorSet = el::CharSet::fromPattern(" ,.!?\n«»“”"_el);
// Create a view into the story literal.
const auto text = el::String{cStory};
// Prepare a list of words and their character/code point lengths.
using WordAndLength = std::pair<el::String, el::CpCount>;
auto wordList = std::vector<WordAndLength>{};
// Scan through the text, extracting words and skipping separators.
// Note: there is a `String::split()` method that splits text with one call.
el::ByteIndex pos = text.indexAt(el::StringSide::Front);
while (pos < text.indexAt(el::StringSide::Back)) {
auto wordStart = text.findFirstNotOf(separatorSet, pos);
if (wordStart.isNoIndex()) {
break;
}
pos = text.findFirstOf(separatorSet, wordStart);
const auto word = text.slice(el::ByteRange{wordStart, pos});
wordList.emplace_back(word, word.characterLength());
}
// Sort the words by length, then lexicographically.
std::ranges::stable_sort(wordList, [](const WordAndLength &a, const WordAndLength &b) -> bool {
if (a.second == b.second) {
return a.first < b.first;
}
return a.second < b.second;
});
// Print the sorted list of words.
el::io::printLine("Sorted word list:"_el);
const auto integerFormat = el::IntegerFormat::decimal().setFieldWidth(el::CpCount{4});
auto lastLength = el::CpCount::infinite();
for (const auto &[word, length] : wordList) {
if (length != lastLength) {
lastLength = length;
el::io::print("\n"_el, integerFormat, length.toRawValue(), ": "_el);
} else {
el::io::print(", "_el);
}
el::io::print(word);
}
el::io::printLine();
// The memory debug view exposes both the visible range identity and the backing storage identity.
// The original text and the selected word have different ranges but the same backing storage.
constexpr auto debugDetails = el::DebugViewDetail::BackingStore;
el::io::printLine(
"The output below shows the memory view of the original text and three selected words.\n"_el,
"Compare the field \"backingStorageId\" - all strings share the original literal data.\n\n"_el,
"Original text:\n"_el,
el::toDebugString(text, debugDetails),
"\nView of three random words:\n"_el);
for (auto i = 0; i < 3; ++i) {
const auto index = el::application().random().selectInteger<std::size_t>(std::size_t{0}, wordList.size());
const auto &entry = wordList.at(index);
el::io::printLine(
"Word "_el, index, ": \""_el, entry.first, "\", (length: "_el, entry.second.toRawValue(), ")"_el);
el::io::printLine(el::toDebugString(wordList.at(index).first, debugDetails));
}
}
Output
Sorted word list:
1: 🌲, 🍄
2: Im, La, an, in
3: Die, Léa, Sie, Und, Wei, auf, auf, auf, das, dem, den, den, den, den, den, des, die,
die, die, ein, und, und, und, von, zum, 🌧️🌲
4: Dann, Doch, Wald, Wald, doch, fiel, noch, noch, rief, Äste, Über
5: Ayumi, Neben, Pilze, Sofia, Stein, Vögel, alten, alten, blieb, durch, einem, einer,
einer, hohen, legte, leise, sagte, zogen, Ästen
6: Binnen, Bodens, Bächen, Farnen, Gläser, Gruppe, Himmel, Javier, Mehmet, Steine, arri
ve, hinter, kleine, liefen, offene, rannte, sangen, stehen, voller, vorbei, warmer, weiter
, weiter, zeigte
7: Blätter, Camille, Fichten, Tropfen, Wurzeln, bientôt, deutete, dunklen, einfach, kle
inen, lachend, niemand, schwere, stillen, tempête, weniger, während, während, 观察风向变化
!
8: Sekunden, hindurch, notierte, rauschte, sammelte, zwischen, zwischen
9: Notizbuch, Plötzlich, einzelner, tanzenden
10: Temperatur, leuchtende, reparierte
11: Baumwipfeln, Regenwolken, Sommerregen, glitzernder, nummerierte
13: Observatorium, moosbedeckten
26: Windgeschwindigkeitsmesser
The output below shows the memory view of the original text and three selected words.
Compare the field "backingStorageId" - all strings share the original literal data.
Original text:
U8String:
backingStorageId: 0x26235705d622490d:0x1a0ad4772acc25cc
View of three random words:
Word 123: "nummerierte", (length: 11)
U8String:
backingStorageId: 0x26235705d622490d:0x1a0ad4772acc25cc
Word 1: "🍄", (length: 1)
U8String:
backingStorageId: 0x26235705d622490d:0x1a0ad4772acc25cc
Word 0: "🌲", (length: 1)
U8String:
backingStorageId: 0x26235705d622490d:0x1a0ad4772acc25cc
Complete demo sources
text/String/IdealFunctionParameter.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/Char.hpp>
#include <erbsland/LoopStatus.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringEditor.hpp>
#include <erbsland/UnicodeCategory.hpp>
namespace demo {
void countEmojis(const el::String &text);
/// `String` is the preferred parameter type for functions that read text.
///
/// It accepts common string inputs naturally:
/// - string literals are used directly without copying,
/// - existing strings share their storage,
/// - local editors convert safely to read-only strings.
///
/// From the caller's perspective, all variants behave the same.
void idealFunctionParameter() {
// A string literal can be passed directly.
countEmojis("🌲🌲 Waldkonzert mit Fuchs 🦊 und Eule 🦉"_el);
// An existing string can be passed without copying.
const auto string = el::String{"Pluie douce sur les fleurs 🌧️🌷🌼"_el};
countEmojis(string);
// A short value assembled with an editor is accepted as read-only input.
auto constructed = el::StringEditor{"Bosque nocturno: "_el};
constructed.append("luna 🌙 y grillos 🦗"_el);
countEmojis(constructed);
}
/// Count all emoji-like symbols in `text` and print the result.
///
/// The function only needs read-only access to the text. It does not need to know
/// whether the caller passed a literal, a string, or a short constructed value.
void countEmojis(const el::String &text) {
std::size_t emojiCount = 0;
text.forEach([&](const el::Char character) mutable noexcept -> el::util::LoopStatus {
if (character.isCategory(el::UnicodeCategory::OtherSymbol)) {
++emojiCount;
}
return el::LoopStatus::Continue;
});
el::io::printLine("Text ..........: \""_el, text, "\""_el);
el::io::printLine("Emoji symbols .: ", emojiCount);
el::io::printLine();
}
}
text/String/ConstructionAndStorage.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include "SharedTools.hpp"
#include <DemoCommon.hpp>
#include <erbsland/ByteRange.hpp>
#include <erbsland/CharSet.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringEditor.hpp>
#include <erbsland/StringSide.hpp>
#include <erbsland/UnicodeCategoryGroup.hpp>
namespace demo {
/// Get the first word in the given text with letters.
auto getFirstWordRange(const el::String &text) -> el::ByteRange {
static const auto separator = el::CharSet::from(el::UnicodeCategoryGroup::Letter);
const auto begin = text.findFirstOf(separator);
if (begin.isNoIndex()) {
return {};
}
auto end = text.findFirstNotOf(separator, begin);
if (end.isNoIndex()) {
end = text.indexAt(el::StringSide::Back);
}
return el::ByteRange{begin, end};
}
/// This demo demonstrates how a `String` can be constructed from various sources and how it behaves.
void constructionAndStorage() {
// Constructing a `String` will just store a safe reference to the string literal.
const auto textForRead = el::String{"🌲 Schwarzwald: kühle Morgenluft"_el};
el::io::printLine("A string from a string literal: "_el, textForRead);
printMemoryAndRangeInfo(textForRead);
// A local editor is appropriate when construction and in-place editing are explicit parts of the workflow.
auto constructedText = el::StringEditor{"🌴 Bali: "_el};
constructedText.append("hangatnya angin fajar"_el);
el::io::printLine("A simply constructed string: "_el, constructedText);
printMemoryAndRangeInfo(constructedText);
// `String`s are created implicitly from `StringEditor` objects.
// The read-only string stores an owning reference to the editor's storage.
// Even when the editor is destroyed, the string and the storage remain valid.
const el::String stringFromEditor = constructedText;
// `String`s are also created implicitly from `StringLiteral` objects.
// That makes them the primary choice for function parameters and to store strings.
const el::String stringFromLiteral = "🌳 Forêt humide après la pluie"_el;
// The strings look identical but carry different storage references.
// From a user perspective, there is no difference in behavior.
el::io::printLine("String from editor : "_el, stringFromEditor);
printMemoryAndRangeInfo(stringFromEditor);
el::io::printLine("String from literal : "_el, stringFromLiteral);
printMemoryAndRangeInfo(stringFromLiteral);
// When sliced, strings keep the same storage reference but change their visible range.
auto word = stringFromEditor.slice(getFirstWordRange(stringFromEditor));
el::io::printLine("First word #1 : "_el, word);
printMemoryAndRangeInfo(word);
word = stringFromLiteral.slice(getFirstWordRange(stringFromLiteral));
el::io::printLine("First word #2 : "_el, word);
printMemoryAndRangeInfo(word);
}
}
text/String/EfficientLiteralUsage.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/ByteIndex.hpp>
#include <erbsland/ByteRange.hpp>
#include <erbsland/CharSet.hpp>
#include <erbsland/CpCount.hpp>
#include <erbsland/IntegerFormat.hpp>
#include <erbsland/Random.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringDebug.hpp>
#include <erbsland/StringSide.hpp>
#include <algorithm>
#include <ranges>
namespace demo {
constexpr static auto cStory = "🌲 Im stillen Wald hinter dem alten Observatorium sammelte Léa leuchtende Blätter, "
"während Mehmet den Windgeschwindigkeitsmesser reparierte. Über den Baumwipfeln "
"zogen schwere Regenwolken auf, doch zwischen den Ästen sangen die Vögel weiter. "
"«La tempête arrive bientôt», sagte Camille leise und zeigte zum dunklen Himmel. "
"Neben einem moosbedeckten Stein notierte Javier die Temperatur des Bodens, "
"während Ayumi kleine Pilze in nummerierte Gläser legte 🍄.\n"
"Plötzlich fiel ein einzelner Tropfen auf das offene Notizbuch von Sofia.\n"
"Dann noch einer. Und noch einer.\n"
"Binnen weniger Sekunden rauschte warmer Sommerregen durch den Wald. "
"Die Gruppe rannte lachend zwischen den hohen Fichten hindurch, "
"vorbei an Farnen, alten Wurzeln und kleinen Bächen voller glitzernder Steine.\n"
"“观察风向变化!” rief Wei und deutete auf die tanzenden Äste.\n"
"Doch niemand blieb stehen.\n"
"Sie liefen einfach weiter 🌧️🌲."_el;
/// `String` only copies a string when there is no alternative.
///
/// - By default, it keeps an owning reference to the original string and stores a range into that string.
/// - For string literals in read-only memory, where this is safe, it simply references the literal data.
///
/// This demo splices individual words from a long string literal and sorts them by character length.
/// The debug output shows that the word views select different ranges in the same backing literal storage.
///
void efficientLiteralUsage() {
// Prepare a set containing all separators used in the text.
static auto separatorSet = el::CharSet::fromPattern(" ,.!?\n«»“”"_el);
// Create a view into the story literal.
const auto text = el::String{cStory};
// Prepare a list of words and their character/code point lengths.
using WordAndLength = std::pair<el::String, el::CpCount>;
auto wordList = std::vector<WordAndLength>{};
// Scan through the text, extracting words and skipping separators.
// Note: there is a `String::split()` method that splits text with one call.
el::ByteIndex pos = text.indexAt(el::StringSide::Front);
while (pos < text.indexAt(el::StringSide::Back)) {
auto wordStart = text.findFirstNotOf(separatorSet, pos);
if (wordStart.isNoIndex()) {
break;
}
pos = text.findFirstOf(separatorSet, wordStart);
const auto word = text.slice(el::ByteRange{wordStart, pos});
wordList.emplace_back(word, word.characterLength());
}
// Sort the words by length, then lexicographically.
std::ranges::stable_sort(wordList, [](const WordAndLength &a, const WordAndLength &b) -> bool {
if (a.second == b.second) {
return a.first < b.first;
}
return a.second < b.second;
});
// Print the sorted list of words.
el::io::printLine("Sorted word list:"_el);
const auto integerFormat = el::IntegerFormat::decimal().setFieldWidth(el::CpCount{4});
auto lastLength = el::CpCount::infinite();
for (const auto &[word, length] : wordList) {
if (length != lastLength) {
lastLength = length;
el::io::print("\n"_el, integerFormat, length.toRawValue(), ": "_el);
} else {
el::io::print(", "_el);
}
el::io::print(word);
}
el::io::printLine();
// The memory debug view exposes both the visible range identity and the backing storage identity.
// The original text and the selected word have different ranges but the same backing storage.
constexpr auto debugDetails = el::DebugViewDetail::BackingStore;
el::io::printLine(
"The output below shows the memory view of the original text and three selected words.\n"_el,
"Compare the field \"backingStorageId\" - all strings share the original literal data.\n\n"_el,
"Original text:\n"_el,
el::toDebugString(text, debugDetails),
"\nView of three random words:\n"_el);
for (auto i = 0; i < 3; ++i) {
const auto index = el::application().random().selectInteger<std::size_t>(std::size_t{0}, wordList.size());
const auto &entry = wordList.at(index);
el::io::printLine(
"Word "_el, index, ": \""_el, entry.first, "\", (length: "_el, entry.second.toRawValue(), ")"_el);
el::io::printLine(el::toDebugString(wordList.at(index).first, debugDetails));
}
}
}
text/String/StorageIdentifier.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/BooleanFormat.hpp>
#include <erbsland/ByteCount.hpp>
#include <erbsland/ByteIndex.hpp>
#include <erbsland/ByteRange.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/StorageIdentifier.hpp>
#include <erbsland/String.hpp>
namespace demo {
/// `storageId()` lets low-level code verify that a cached native index still
/// belongs to the same visible storage range.
///
/// This is useful when a byte index outlives the immediate operation that
/// produced it. Even when two strings contain the same decoded text, a native
/// index from one storage range must not be applied to another one.
void storageIdentifier() {
struct CachedRange final {
el::StorageIdentifier storageId;
el::ByteIndex index;
el::ByteCount length;
};
const auto report = el::String{"温度計A: 21℃; 気圧計B: 1012hPa; 湿度計C: 45%"_el};
const auto token = el::String{"気圧計"_el};
const auto cachedToken = CachedRange{report.storageId(), report.find(token), token.length()};
const auto booleanFormat = el::BooleanFormat::yesNo();
const auto tryUseCachedRange = [&](const el::String &label, const el::String &candidate) -> void {
const auto sameStorage = candidate.storageId() == cachedToken.storageId;
el::io::printLine(label, ":"_el);
el::io::printLine(" same visible storage range: "_el, booleanFormat, sameStorage);
if (sameStorage && !cachedToken.index.isNoIndex()) {
el::io::printLine(
" cached range reads ........: "_el,
candidate.slice(el::ByteRange{cachedToken.index, cachedToken.length}));
} else {
el::io::printLine(" cached range reads ........: <not used>"_el);
}
};
const auto copiedReport = report.copy();
const auto tail = report.slice(el::ByteRange{report.find(token), el::ByteCount::infinite()});
el::io::printLine("Report: "_el, report);
el::io::printLine("Cached token: "_el, token);
el::io::printLine("Cached byte index: "_el, cachedToken.index);
el::io::printLine();
tryUseCachedRange("Original string"_el, report);
tryUseCachedRange("Copied text"_el, copiedReport);
tryUseCachedRange("Tail slice"_el, tail);
}
}