Working with Paths
This page explains the platform-independent features of
Path.
You will learn how to construct, inspect, compare, modify, join, slice, and convert paths without accessing the
filesystem.
These operations work entirely on the logical structure of a path, making them deterministic and independent of the
operating system.
Constructing Paths
Create a Path directly when the path text originates from your own application and
should use the library’s automatic format detection.
The constructor recognizes POSIX roots, Windows drive roots, and UNC roots, then stores the result in a normalized,
platform-independent representation.
When the external format is already known, use
fromPosix() or
fromWindows() instead.
This avoids ambiguities.
For example, c:/manifestos is interpreted as a relative POSIX path when parsed as POSIX text, but as an absolute
Windows path when parsed as Windows text.
Invalid or structurally malformed input results in an empty path.
If invalid input should instead report an error, use the corresponding throwing functions such as
fromPosixOrThrow(),
fromWindowsOrThrow(),
or
fromNativeOrThrow().
path/WorkingWithPaths/ConstructionAndFormats.cpp
/// Construct paths from generic, POSIX, and Windows text.
///
/// `Path` stores platform-independent path data. Generic construction recognizes common root forms and normalizes
/// separators to slash characters. Use `fromPosix()` or `fromWindows()` when text comes from a known external format
/// and must not be interpreted through the generic auto-detection rules.
void constructionAndFormats() {
// Generic construction normalizes repeated separators.
const auto logBook = el::Path{"expedicao//ceu/diario.txt"_el};
el::io::printLine("generic ...........: "_el, logBook.toString());
// POSIX parsing treats a drive-looking prefix as ordinary relative text.
const auto posixManifest = el::Path::fromPosix("c:/manifestos/vento.txt"_el);
el::io::printLine("posix .............: "_el, posixManifest.toString());
el::io::printLine("relative ..........: "_el, el::BooleanFormat::yesNo(), posixManifest.isRelative());
// Windows parsing accepts backslashes and normalizes drive letters.
const auto windowsChart = el::Path::fromWindows("C:\\Expedicao\\rotas\\manha.txt"_el);
el::io::printLine("windows ...........: "_el, windowsChart.toString());
// UNC roots keep the share name as written, while the server name is normalized.
const auto sharedChart = el::Path::fromWindows("\\\\PORTO-CEU\\Mapas\\norte.txt"_el);
el::io::printLine("unc root ..........: "_el, sharedChart.root());
}
Output
generic ...........: expedicao/ceu/diario.txt
posix .............: c:/manifestos/vento.txt
relative ..........: yes
windows ...........: c:/Expedicao/rotas/manha.txt
unc root ..........: //porto-ceu/Mapas/
Inspecting Paths
Before working with a path, you often need to know whether it names a location relative to another directory or starts
at a root.
The isRelative(),
isAbsolute(), and
isRoot() functions answer these questions from the path itself.
Absolute paths expose their root as the first public path element.
For example, the path /arquivo/rotas.txt consists of the root / followed by the elements arquivo and
rotas.txt.
Treating the root as part of the public element sequence keeps indexing and slicing explicit.
A slice that contains the first element remains absolute, while a slice that starts after the root becomes relative.
For a report or archive path, you may also need the file name or its extension.
The name(),
stem(),
suffix(), and
suffixes() functions inspect the final non-root element.
Leading dots are considered part of the file name rather than suffix separators, allowing hidden files to behave as
expected.
path/WorkingWithPaths/InspectingPaths.cpp
/// Inspect roots, elements, names, suffixes, and parent paths.
///
/// A path can be queried without touching the filesystem. Roots are part of the public element sequence, while names,
/// stems, and suffixes operate on the final non-root element. This makes metadata-style path work deterministic and
/// independent from the current operating system.
void inspectingPaths() {
const auto report = el::Path{"/arquivo/rotas/relatorio.final.txt"_el};
// Basic structure and naming information.
el::io::printLine("path ..............: "_el, report.toString());
el::io::printLine("root ..............: "_el, report.root());
el::io::printLine("name ..............: "_el, report.name());
el::io::printLine("stem ..............: "_el, report.stem());
el::io::printLine("suffixes ..........: "_el, report.suffixes());
// Public elements include the root for absolute paths.
for (auto index = el::ItemIndex{}; index.isWithin(report.elementCount()); ++index) {
el::io::printLine("element "_el, index.toSizeT(), " .........: "_el, report.element(index));
}
// Parent paths are ordered from nearest to furthest.
for (const auto &parent : report.parents()) {
el::io::printLine("parent ............: "_el, parent.toString());
}
}
Output
path ..............: /arquivo/rotas/relatorio.final.txt
root ..............: /
name ..............: relatorio.final.txt
stem ..............: relatorio
suffixes ..........: .final.txt
element 0 .........: /
element 1 .........: arquivo
element 2 .........: rotas
element 3 .........: relatorio.final.txt
parent ............: /arquivo/rotas
parent ............: /arquivo
parent ............: /
Comparing Paths
When you collect report paths or check for duplicate entries in a file list, you can compare
Path values directly with == and the ordering operators.
Like inspection, comparison works on the root and path elements without accessing the filesystem.
The files do not need to exist, and you do not need to allocate strings by converting the paths to text first.
The compare() method provides the same ordering as a
std::strong_ordering result.
Sorting paths follows their hierarchy: comparison starts with the roots, then proceeds through the elements until it
finds a difference.
A parent sorts before its descendants, so rotas comes before rotas/manha.txt.
That child also comes before rotas-antigas, because the first element rotas sorts before rotas-antigas.
Sorting the text returned by toString() would give a different order,
because the separators would participate in the string comparison.
Empty paths sort first, followed by relative paths and then absolute paths.
Within each root or element, comparison uses case-sensitive Unicode code-point order.
If your application’s file list should ignore case, you can pass
Char::compareCaseFolded() to compare().
This changes the character matching rule while keeping the same hierarchy.
The Comparing Whole and Partial Strings topic explains the available character comparison rules.
Path equality answers whether the path values match; identifying an existing file requires filesystem information.
For example, rotas/./manha.txt and rotas/manha.txt compare unequal because comparison preserves the .
element.
Links or filesystem-specific case rules may also let different paths refer to the same file.
Case folding alone cannot reproduce those rules.
See Path API Overview for the filesystem operations that complement these path-value operations.
Editing Names
The name-editing methods return a new Path while leaving the original object
unchanged.
Use
withName() to replace the final
path element.
Use
withStem() when only the base name
changes while the suffixes remain unchanged.
Use
withSuffix() to replace or remove
the suffixes without rebuilding the complete path.
These functions are particularly useful when deriving related files such as reports, archives, generated documentation, or companion resources from a common base path.
path/WorkingWithPaths/EditingPathNames.cpp
/// Edit the final path element without rebuilding the whole path string.
///
/// `withName()`, `withSuffix()`, and `withStem()` return modified paths and leave the original path unchanged. They
/// are useful when an application derives companion files, report formats, or archive names from a common base path.
void editingPathNames() {
const auto baseReport = el::Path{"expedicao/diario/vento.final.txt"_el};
// Create related names from one base report.
const auto publicReport = baseReport.withName("relatorio.txt"_el);
const auto markdownReport = baseReport.withSuffix("md"_el);
const auto archiveReport = baseReport.withStem("arquivo-vento"_el);
const auto plainName = baseReport.withSuffix({}); // remove all suffixes
el::io::printLine("base ..............: "_el, baseReport.toString());
el::io::printLine("name ..............: "_el, publicReport.toString());
el::io::printLine("suffix ............: "_el, markdownReport.toString());
el::io::printLine("stem ..............: "_el, archiveReport.toString());
el::io::printLine("no suffix .........: "_el, plainName.toString());
}
Output
base ..............: expedicao/diario/vento.final.txt
name ..............: expedicao/diario/relatorio.txt
suffix ............: expedicao/diario/vento.md
stem ..............: expedicao/diario/arquivo-vento.final.txt
no suffix .........: expedicao/diario/vento
Joining and Slicing
Build larger paths by combining smaller path fragments with
join(),
joined(), or the / operator.
join() modifies the path, while joined() and / return a new path.
String arguments use generic path parsing, where relative text treats both slashes and backslashes as separators.
For text obtained from the native filesystem, use joinNative() or
joinedNative() instead.
These methods use the current platform’s path syntax, preserving a backslash inside a filename on POSIX systems.
When the right-hand path is absolute, only its non-root elements are appended. This allows reusable path fragments to be combined safely regardless of whether they were originally parsed as absolute or relative paths.
Use
slice() to extract a range of public
path elements.
The resulting path preserves its absolute nature only when the slice begins with the root element.
Otherwise, the result becomes a relative path.
Because slicing operates on the same public element sequence exposed by the inspection functions, it behaves predictably across all supported path formats.
path/WorkingWithPaths/JoiningAndSlicing.cpp
/// Join paths and extract element ranges.
///
/// Joining appends the non-root elements of the right-hand path, even when that path is absolute. Slicing works on the
/// public element sequence. If the slice starts with the root element, the result remains absolute; otherwise it
/// becomes a relative path.
void joiningAndSlicing() {
const auto archive = el::Path{"/arquivo/expedicoes"_el};
const auto morningLog = archive / "ceu-leste"_el / "manha.txt"_el;
const auto borrowedAbsolute = archive / el::Path{"/rotas/noite.txt"_el};
// Build paths from reusable fragments.
el::io::printLine("joined ............: "_el, morningLog.toString());
el::io::printLine("absolute rhs ......: "_el, borrowedAbsolute.toString());
// Slice with and without the root element.
const auto absoluteSlice = morningLog.slice(el::ItemRange{el::ItemIndex{0}, el::ItemCount{3}});
const auto relativeSlice = morningLog.slice(el::ItemRange{el::ItemIndex{1}, el::ItemCount{2}});
el::io::printLine("absolute slice ....: "_el, absoluteSlice.toString());
el::io::printLine("relative slice ....: "_el, relativeSlice.toString());
}
Output
joined ............: /arquivo/expedicoes/ceu-leste/manha.txt
absolute rhs ......: /arquivo/expedicoes/rotas/noite.txt
absolute slice ....: /arquivo/expedicoes
relative slice ....: arquivo/expedicoes
Converting Paths
Use
toString() whenever you need a
platform-independent textual representation.
It always uses forward slashes and is therefore well suited for logging, configuration files, serialization, and
diagnostics.
Use
toPosix() or
toWindows() only when an
external interface explicitly requires one of these formats.
Not every absolute path can be represented in every external syntax.
For example, an absolute Windows path cannot be converted into a POSIX path, and an absolute POSIX path cannot be
converted into a Windows path.
Use
toStdPath() when interoperating
with APIs that require std::filesystem::path.
The path manipulation facilities of this library do not depend on the standard filesystem library, so conversion is only
necessary at API boundaries.
path/WorkingWithPaths/ConvertingPaths.cpp
/// Convert paths back to display, POSIX, Windows, and standard-library forms.
///
/// `toString()` is the preferred platform-independent display form. `toPosixPath()` and `toWindowsPath()` are for
/// explicit external formats, while `toStdPath()` is reserved for interoperability with code that expects a standard
/// library path object.
void convertingPaths() {
const auto relative = el::Path{"expedicao/rotas/manha.txt"_el};
const auto windows = el::Path::fromWindows("C:\\Expedicao\\rotas\\manha.txt"_el);
const auto posix = el::Path::fromPosix("/arquivo/rotas/manha.txt"_el);
// Relative paths can be emitted in either text format.
el::io::printLine("display ...........: "_el, relative.toString());
el::io::printLine("posix ...........: "_el, relative.toPosix());
el::io::printLine("windows ...........: "_el, relative.toWindows(el::PathWindowsFormat::Native));
// Absolute Windows and POSIX roots stay in their own external format.
el::io::printLine("drive ...........: "_el, windows.toWindows(el::PathWindowsFormat::Native));
el::io::printLine("posix / ...........: "_el, posix.toPosix());
// Use std::filesystem interop only when another API requires it.
const auto stdPath = relative.toStdPath();
el::io::printLine("std ...............: "_el, el::StringConverter{stdPath.generic_string()}.toString());
}
Output
display ...........: expedicao/rotas/manha.txt
posix ...........: expedicao/rotas/manha.txt
windows ...........: expedicao\rotas\manha.txt
drive ...........: c:\Expedicao\rotas\manha.txt
posix / ...........: /arquivo/rotas/manha.txt
std ...............: expedicao/rotas/manha.txt
Complete demo sources
path/WorkingWithPaths/ConstructionAndFormats.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include "WorkingWithPathsDemos.hpp"
#include <DemoCommon.hpp>
#include <erbsland/BooleanFormat.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/StandardStreams.hpp>
namespace demo {
/// Construct paths from generic, POSIX, and Windows text.
///
/// `Path` stores platform-independent path data. Generic construction recognizes common root forms and normalizes
/// separators to slash characters. Use `fromPosix()` or `fromWindows()` when text comes from a known external format
/// and must not be interpreted through the generic auto-detection rules.
void constructionAndFormats() {
// Generic construction normalizes repeated separators.
const auto logBook = el::Path{"expedicao//ceu/diario.txt"_el};
el::io::printLine("generic ...........: "_el, logBook.toString());
// POSIX parsing treats a drive-looking prefix as ordinary relative text.
const auto posixManifest = el::Path::fromPosix("c:/manifestos/vento.txt"_el);
el::io::printLine("posix .............: "_el, posixManifest.toString());
el::io::printLine("relative ..........: "_el, el::BooleanFormat::yesNo(), posixManifest.isRelative());
// Windows parsing accepts backslashes and normalizes drive letters.
const auto windowsChart = el::Path::fromWindows("C:\\Expedicao\\rotas\\manha.txt"_el);
el::io::printLine("windows ...........: "_el, windowsChart.toString());
// UNC roots keep the share name as written, while the server name is normalized.
const auto sharedChart = el::Path::fromWindows("\\\\PORTO-CEU\\Mapas\\norte.txt"_el);
el::io::printLine("unc root ..........: "_el, sharedChart.root());
}
}
path/WorkingWithPaths/InspectingPaths.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include "WorkingWithPathsDemos.hpp"
#include <DemoCommon.hpp>
#include <erbsland/ItemIndex.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/StandardStreams.hpp>
namespace demo {
/// Inspect roots, elements, names, suffixes, and parent paths.
///
/// A path can be queried without touching the filesystem. Roots are part of the public element sequence, while names,
/// stems, and suffixes operate on the final non-root element. This makes metadata-style path work deterministic and
/// independent from the current operating system.
void inspectingPaths() {
const auto report = el::Path{"/arquivo/rotas/relatorio.final.txt"_el};
// Basic structure and naming information.
el::io::printLine("path ..............: "_el, report.toString());
el::io::printLine("root ..............: "_el, report.root());
el::io::printLine("name ..............: "_el, report.name());
el::io::printLine("stem ..............: "_el, report.stem());
el::io::printLine("suffixes ..........: "_el, report.suffixes());
// Public elements include the root for absolute paths.
for (auto index = el::ItemIndex{}; index.isWithin(report.elementCount()); ++index) {
el::io::printLine("element "_el, index.toSizeT(), " .........: "_el, report.element(index));
}
// Parent paths are ordered from nearest to furthest.
for (const auto &parent : report.parents()) {
el::io::printLine("parent ............: "_el, parent.toString());
}
}
}
path/WorkingWithPaths/EditingPathNames.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include "WorkingWithPathsDemos.hpp"
#include <DemoCommon.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/StandardStreams.hpp>
namespace demo {
/// Edit the final path element without rebuilding the whole path string.
///
/// `withName()`, `withSuffix()`, and `withStem()` return modified paths and leave the original path unchanged. They
/// are useful when an application derives companion files, report formats, or archive names from a common base path.
void editingPathNames() {
const auto baseReport = el::Path{"expedicao/diario/vento.final.txt"_el};
// Create related names from one base report.
const auto publicReport = baseReport.withName("relatorio.txt"_el);
const auto markdownReport = baseReport.withSuffix("md"_el);
const auto archiveReport = baseReport.withStem("arquivo-vento"_el);
const auto plainName = baseReport.withSuffix({}); // remove all suffixes
el::io::printLine("base ..............: "_el, baseReport.toString());
el::io::printLine("name ..............: "_el, publicReport.toString());
el::io::printLine("suffix ............: "_el, markdownReport.toString());
el::io::printLine("stem ..............: "_el, archiveReport.toString());
el::io::printLine("no suffix .........: "_el, plainName.toString());
}
}
path/WorkingWithPaths/JoiningAndSlicing.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include "WorkingWithPathsDemos.hpp"
#include <DemoCommon.hpp>
#include <erbsland/ItemCount.hpp>
#include <erbsland/ItemIndex.hpp>
#include <erbsland/ItemRange.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/StandardStreams.hpp>
namespace demo {
/// Join paths and extract element ranges.
///
/// Joining appends the non-root elements of the right-hand path, even when that path is absolute. Slicing works on the
/// public element sequence. If the slice starts with the root element, the result remains absolute; otherwise it
/// becomes a relative path.
void joiningAndSlicing() {
const auto archive = el::Path{"/arquivo/expedicoes"_el};
const auto morningLog = archive / "ceu-leste"_el / "manha.txt"_el;
const auto borrowedAbsolute = archive / el::Path{"/rotas/noite.txt"_el};
// Build paths from reusable fragments.
el::io::printLine("joined ............: "_el, morningLog.toString());
el::io::printLine("absolute rhs ......: "_el, borrowedAbsolute.toString());
// Slice with and without the root element.
const auto absoluteSlice = morningLog.slice(el::ItemRange{el::ItemIndex{0}, el::ItemCount{3}});
const auto relativeSlice = morningLog.slice(el::ItemRange{el::ItemIndex{1}, el::ItemCount{2}});
el::io::printLine("absolute slice ....: "_el, absoluteSlice.toString());
el::io::printLine("relative slice ....: "_el, relativeSlice.toString());
}
}
path/WorkingWithPaths/ConvertingPaths.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include "WorkingWithPathsDemos.hpp"
#include <DemoCommon.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/PathWindowsFormat.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringConverter.hpp>
namespace demo {
/// Convert paths back to display, POSIX, Windows, and standard-library forms.
///
/// `toString()` is the preferred platform-independent display form. `toPosixPath()` and `toWindowsPath()` are for
/// explicit external formats, while `toStdPath()` is reserved for interoperability with code that expects a standard
/// library path object.
void convertingPaths() {
const auto relative = el::Path{"expedicao/rotas/manha.txt"_el};
const auto windows = el::Path::fromWindows("C:\\Expedicao\\rotas\\manha.txt"_el);
const auto posix = el::Path::fromPosix("/arquivo/rotas/manha.txt"_el);
// Relative paths can be emitted in either text format.
el::io::printLine("display ...........: "_el, relative.toString());
el::io::printLine("posix ...........: "_el, relative.toPosix());
el::io::printLine("windows ...........: "_el, relative.toWindows(el::PathWindowsFormat::Native));
// Absolute Windows and POSIX roots stay in their own external format.
el::io::printLine("drive ...........: "_el, windows.toWindows(el::PathWindowsFormat::Native));
el::io::printLine("posix / ...........: "_el, posix.toPosix());
// Use std::filesystem interop only when another API requires it.
const auto stdPath = relative.toStdPath();
el::io::printLine("std ...............: "_el, el::StringConverter{stdPath.generic_string()}.toString());
}
}