Accessing Configuration Values

After you locate a value in a configuration tree, you still need to turn it into a useful application type. The Value interface offers two complementary approaches: as...() methods interpret the current value, while get...() methods resolve a child and interpret it in one step.

This page explains both approaches, including their error behavior, template forms, and list handling. For everyday application settings, prefer the typed get...() methods. Use direct as...() access when you already have a value object, for example while iterating through an unknown tree or accepting several deliberate representations.

Convert the Current Value

Every scalar ELCL type has a matching pair of accessors. For example, asText() and asTextOrThrow() access text, while asInteger() and asIntegerOrThrow() access an integer. Equivalent pairs exist for booleans, floating-point values, dates, times, date-times, byte data, time deltas, and regular expressions.

These methods do not coerce between ELCL types. An integer cannot be read as text, and text containing digits cannot be read as an integer. The non-throwing form returns the type’s default-constructed value after a mismatch, such as an empty string or zero. The OrThrow form raises ConfError with the value’s name path and source location. Throwing access is therefore the safer choice for required data: an actual zero cannot be confused with a failed conversion.

conf/IndividualValues/ScalarConversions.cpp

/// Convert one configuration value to its native C++ type.
///
/// The named `as...()` methods make the expected type visible, while `asType<T>()` is useful in generic code and for
/// converting an ELCL integer to a narrower native integer. The `OrThrow` forms report type and range mismatches.
void scalarConversions() {
    const auto configuration = "[survey]\n"
                               "forest: \"Mata Atlântica\"\n"
                               "maximum_observers: 18\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);
    const auto forestValue = document->valueOrThrow("survey.forest"_el);
    const auto observerValue = document->valueOrThrow("survey.maximum_observers"_el);

    // Use a named conversion when the configuration type is known at the call site.
    const auto forest = forestValue->asTextOrThrow();

    // Template conversion can select a narrower native representation for an integer.
    const auto maximumObservers = observerValue->asTypeOrThrow<std::uint16_t>();
    el::io::printLine(forest, ": up to "_el, maximumObservers, " observers"_el);

    // A throwing conversion preserves a type mismatch instead of hiding it as a default value.
    try {
        (void)observerValue->asTextOrThrow();
    } catch (const el::conf::ConfError &) {
        el::io::printLine("An integer cannot be accessed as text."_el);
    }
}

Output

Mata Atlântica: up to 18 observers
An integer cannot be accessed as text.

The template forms asType() and asTypeOrThrow() are useful when the expected C++ type is itself a template argument. They also let you select a native integer or floating-point width. For a target integer that is too narrow, asType<T>() saturates at the target type’s limit, while asTypeOrThrow<T>() reports the range mismatch. That distinction makes the throwing form preferable when truncation would change the meaning of a setting.

Choose a Structural View for Lists

A value list can be viewed either as its original ValueList or as a native, type-checked collection. asValueList() is deliberately strict: it succeeds only when the current node is a value list. It does not wrap a scalar in a one-element list. The returned values retain their names, locations, and validation metadata, which is useful to diagnostics and generic tree processing.

toValueList() provides a more relaxed structural view. It returns the entries of a value list, wraps a scalar in a one-element list, and returns an empty list for sections and other structural nodes. Similarly, toValueMatrix() turns a scalar into a one-by-one matrix, a flat list into one row per entry, and a nested list into its row-and-column layout. These methods keep Value objects; they normalize shape without converting content.

For native content, asList() and asMatrix() are strict about element types. They accept a scalar of the requested type or a list shape in which every populated element has exactly that ELCL type; they do not coerce mixed lists. The non-throwing forms return an empty collection after any mismatch, while asListOrThrow<T>() and asMatrixOrThrow<T>() identify the offending value.

conf/IndividualValues/ValueLists.cpp

/// Inspect raw value lists and normalize scalar or list-shaped input.
///
/// `asValueList()` strictly requires a value-list node. `toValueList()` and `toValueMatrix()` are deliberately more
/// relaxed: they also wrap scalar values and turn flat or nested lists into a uniform structural view.
void valueLists() {
    const auto configuration = "[survey]\n"
                               "lead_observer: \"Inês\"\n"
                               "species: \"lobo-guará\", \"jaguatirica\", \"capivara\"\n"
                               "observations:\n"
                               "    * 4, 6, 3\n"
                               "    * 2, 5\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);
    const auto speciesValue = document->valueOrThrow("survey.species"_el);

    // Strict list access keeps the individual Value objects and their metadata.
    const auto species = speciesValue->asValueListOrThrow();
    el::io::printLine("First species: "_el, speciesValue->firstValue()->asTextOrThrow());
    el::io::printLine("Last species: "_el, speciesValue->lastValue()->asTextOrThrow());
    el::io::printLine("Species entries: "_el, species.size());

    // Relaxed structural conversion also gives a scalar a one-element list view.
    const auto leadObserver = document->valueOrThrow("survey.lead_observer"_el)->toValueList();
    const auto observationMatrix = document->valueOrThrow("survey.observations"_el)->toValueMatrix();
    el::io::printLine("Lead-observer entries: "_el, leadObserver.size());
    el::io::printLine(
        "Observation matrix: "_el,
        observationMatrix.rowCount(),
        " rows, "_el,
        observationMatrix.columnCount(),
        " columns"_el);
}

Output

First species: lobo-guará
Last species: capivara
Species entries: 3
Lead-observer entries: 1
Observation matrix: 2 rows, 3 columns

empty() tests whether any container has children. firstValue() and lastValue() return its boundary entries, or nullptr when it is empty. They are convenient for a quick inspection; use iteration or indexed access when every entry matters.

Read Settings with Typed Getters

The get...() families combine name-path lookup with type checking. A call such as getText() first resolves the path relative to the current value and then requires the result to be text. If the path is invalid, absent, or has another type, it returns the supplied default. getTextOrThrow() instead reports the problem as a ConfError.

Use a non-throwing getter with a meaningful default for a genuinely optional setting. Use an OrThrow getter when the setting is required or when continuing with a substitute would hide a broken configuration. Named methods such as getText(), getInteger(), and getDateTime() usually produce the clearest application code. The template get() and getOrThrow() variants are helpful in generic code and for selecting a native numeric width.

conf/IndividualValues/TypedGetters.cpp

/// Read application settings with typed convenience getters.
///
/// Named getters produce compact, expressive code for ordinary settings. Template getters support the same path-like
/// inputs and are useful in generic code or when a native integer width must be selected explicitly.
void typedGetters() {
    const auto configuration = "[survey]\n"
                               "forest: \"Floresta da Tijuca\"\n"
                               "observers: 7\n"
                               "species: \"mico-leão-dourado\", \"tucano\"\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);

    // A string path is concise, while a Name or NamePath is convenient when paths are assembled or reused.
    const auto forest = document->getTextOrThrow("survey.forest"_el);
    const auto survey = document->valueOrThrow(el::conf::Name::createRegular("survey"_el));
    const auto observersPath = el::conf::NamePath::fromText("observers"_el);
    const auto observers = survey->getOrThrow<std::uint16_t>(observersPath);

    // An index is the fourth path-like form and addresses an entry in a list directly.
    const auto species = document->valueOrThrow("survey.species"_el);
    const auto firstSpecies = species->getOrThrow<el::String>(std::size_t{0});

    // Non-throwing getters make an application default explicit for optional values.
    const auto season = document->getText("survey.season"_el, "seca"_el);
    el::io::printLine(forest, ": "_el, observers, " observers"_el);
    el::io::printLine("First species: "_el, firstSpecies, "; season: "_el, season);
}

Output

Floresta da Tijuca: 7 observers
First species: mico-leão-dourado; season: seca

All getters accept NamePathLike. As shown in the demo, that can be a string containing a complete path, one Name, a reusable NamePath, or an index into a list. See Working with Configuration Trees for path syntax and guidance on choosing among these forms.

Require Uniform Lists and Matrices

When the application needs native values rather than tree nodes, use getList() or getListOrThrow(). These methods resolve the path and require every list entry to have the requested ELCL type. They also accept one scalar of that type and return a one-element el::List<T>, which lets a setting support both concise and expanded input without special-case code.

The corresponding getMatrix() and getMatrixOrThrow() methods accept scalar, flat-list, and nested-list shapes and return a type-checked Matrix. On any path, shape, or element-type problem, a non-throwing getter returns an empty collection; the OrThrow form identifies the failing value. getList<T>(path, defaultValue) can return a supplied default instead. For text settings, getStringList(path, defaultValue) and getStringListOrThrow(path) return el::StringList directly. When a setting is optional but must have the correct type when present, pass a default to getListOrThrow<T>(path, defaultValue) or getStringListOrThrow(path, defaultValue). These overloads use the default only for a missing path and still report invalid paths or incompatible values. Use asStringList() or asStringListOrThrow() for a text value you have already selected.

conf/IndividualValues/TypedCollections.cpp

/// Read type-checked lists and matrices from a document.
///
/// Typed collection getters resolve the path, verify every element, and return native values. They also accept a scalar
/// of the requested type as a one-element list or matrix, which lets a setting support concise and expanded forms.
void typedCollections() {
    const auto configuration = "[survey]\n"
                               "species: \"onça-pintada\", \"tamanduá-bandeira\"\n"
                               "sample_count: 12\n"
                               "observations:\n"
                               "    * 4, 6, 3\n"
                               "    * 2, 5, 1\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);

    // Typed getters return native collections after checking every element.
    const auto species = document->getStringListOrThrow("survey.species"_el);
    const auto sampleCounts = document->getListOrThrow<int>("survey.sample_count"_el);
    const auto observations = document->getMatrixOrThrow<int>("survey.observations"_el);

    el::io::printLine("Species: "_el, species.count());
    el::io::printLine("Sample-count entries: "_el, sampleCounts.count());
    el::io::printLine("First observation: "_el, observations.valueOrThrow({}, {}));
}

Output

Species: 2
Sample-count entries: 1
First observation: 4

The similarly named APIs serve different layers. value(...)->asValueList() first resolves a value explicitly and then strictly exposes its raw children. getValueList(...) combines those two operations but still requires an actual value-list node. getList<T>(...) additionally checks every element, returns native C++ values, and accepts a matching scalar. The direct asList<T>() and asMatrix<T>() methods perform the same typed conversion on a Value you already hold.

Inspect Types Before Choosing a Representation

Most settings should have one documented type and use a typed getter. When a format intentionally permits alternatives, the is...() methods let you inspect the node without triggering a conversion. For example, isText() and isInteger() can distinguish a named identifier from a numeric one before the appropriate as...OrThrow() call.

conf/IndividualValues/TypeTests.cpp

/// Inspect a value before accepting one of several representations.
///
/// The `is...()` methods make intentional alternatives explicit. Use them when an application genuinely accepts more
/// than one ELCL type; prefer a typed getter when the configuration contract requires exactly one type.
void typeTests() {
    const auto configuration = "[survey]\n"
                               "station: 42\n"
                               "reference_species: \"bugio-ruivo\"\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);

    const auto station = document->valueOrThrow("survey.station"_el);
    if (station->isInteger()) {
        el::io::printLine("Numeric station: "_el, station->asIntegerOrThrow());
    } else if (station->isText()) {
        el::io::printLine("Named station: "_el, station->asTextOrThrow());
    }

    const auto referenceSpecies = document->valueOrThrow("survey.reference_species"_el);
    el::io::printLine("Reference is text: "_el, el::BooleanFormat::yesNo(), referenceSpecies->isText());
    el::io::printLine("Document is a map: "_el, el::BooleanFormat::yesNo(), document->isMap());
}

Output

Numeric station: 42
Reference is text: yes
Document is a map: yes

Tests exist for every scalar and container type. The broader isList() recognizes value lists and section lists, while isMap() recognizes documents and named sections. These broader tests are useful for generic tree tools; application configuration usually benefits from the more precise type tests.

Validate Before Access

Typed access verifies the setting currently being read, but it does not prove that the whole document follows the application’s contract. For larger configurations, Validating Documents with Rule Schemas explains how to check the complete tree, apply defaults, and attach metadata before application code starts reading values.

Complete demo sources

conf/IndividualValues/ScalarConversions.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "IndividualValuesDemos.hpp"

#include <erbsland/conf/ConfError.hpp>
#include <erbsland/conf/Parser.hpp>
#include <erbsland/StandardStreams.hpp>

#include <cstdint>

namespace demo {

/// Convert one configuration value to its native C++ type.
///
/// The named `as...()` methods make the expected type visible, while `asType<T>()` is useful in generic code and for
/// converting an ELCL integer to a narrower native integer. The `OrThrow` forms report type and range mismatches.
void scalarConversions() {
    const auto configuration = "[survey]\n"
                               "forest: \"Mata Atlântica\"\n"
                               "maximum_observers: 18\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);
    const auto forestValue = document->valueOrThrow("survey.forest"_el);
    const auto observerValue = document->valueOrThrow("survey.maximum_observers"_el);

    // Use a named conversion when the configuration type is known at the call site.
    const auto forest = forestValue->asTextOrThrow();

    // Template conversion can select a narrower native representation for an integer.
    const auto maximumObservers = observerValue->asTypeOrThrow<std::uint16_t>();
    el::io::printLine(forest, ": up to "_el, maximumObservers, " observers"_el);

    // A throwing conversion preserves a type mismatch instead of hiding it as a default value.
    try {
        (void)observerValue->asTextOrThrow();
    } catch (const el::conf::ConfError &) {
        el::io::printLine("An integer cannot be accessed as text."_el);
    }
}

}

conf/IndividualValues/ValueLists.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "IndividualValuesDemos.hpp"

#include <erbsland/conf/Parser.hpp>
#include <erbsland/StandardStreams.hpp>

namespace demo {

/// Inspect raw value lists and normalize scalar or list-shaped input.
///
/// `asValueList()` strictly requires a value-list node. `toValueList()` and `toValueMatrix()` are deliberately more
/// relaxed: they also wrap scalar values and turn flat or nested lists into a uniform structural view.
void valueLists() {
    const auto configuration = "[survey]\n"
                               "lead_observer: \"Inês\"\n"
                               "species: \"lobo-guará\", \"jaguatirica\", \"capivara\"\n"
                               "observations:\n"
                               "    * 4, 6, 3\n"
                               "    * 2, 5\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);
    const auto speciesValue = document->valueOrThrow("survey.species"_el);

    // Strict list access keeps the individual Value objects and their metadata.
    const auto species = speciesValue->asValueListOrThrow();
    el::io::printLine("First species: "_el, speciesValue->firstValue()->asTextOrThrow());
    el::io::printLine("Last species: "_el, speciesValue->lastValue()->asTextOrThrow());
    el::io::printLine("Species entries: "_el, species.size());

    // Relaxed structural conversion also gives a scalar a one-element list view.
    const auto leadObserver = document->valueOrThrow("survey.lead_observer"_el)->toValueList();
    const auto observationMatrix = document->valueOrThrow("survey.observations"_el)->toValueMatrix();
    el::io::printLine("Lead-observer entries: "_el, leadObserver.size());
    el::io::printLine(
        "Observation matrix: "_el,
        observationMatrix.rowCount(),
        " rows, "_el,
        observationMatrix.columnCount(),
        " columns"_el);
}

}

conf/IndividualValues/TypedGetters.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "IndividualValuesDemos.hpp"

#include <erbsland/conf/Name.hpp>
#include <erbsland/conf/NamePath.hpp>
#include <erbsland/conf/Parser.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>

#include <cstddef>
#include <cstdint>

namespace demo {

/// Read application settings with typed convenience getters.
///
/// Named getters produce compact, expressive code for ordinary settings. Template getters support the same path-like
/// inputs and are useful in generic code or when a native integer width must be selected explicitly.
void typedGetters() {
    const auto configuration = "[survey]\n"
                               "forest: \"Floresta da Tijuca\"\n"
                               "observers: 7\n"
                               "species: \"mico-leão-dourado\", \"tucano\"\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);

    // A string path is concise, while a Name or NamePath is convenient when paths are assembled or reused.
    const auto forest = document->getTextOrThrow("survey.forest"_el);
    const auto survey = document->valueOrThrow(el::conf::Name::createRegular("survey"_el));
    const auto observersPath = el::conf::NamePath::fromText("observers"_el);
    const auto observers = survey->getOrThrow<std::uint16_t>(observersPath);

    // An index is the fourth path-like form and addresses an entry in a list directly.
    const auto species = document->valueOrThrow("survey.species"_el);
    const auto firstSpecies = species->getOrThrow<el::String>(std::size_t{0});

    // Non-throwing getters make an application default explicit for optional values.
    const auto season = document->getText("survey.season"_el, "seca"_el);
    el::io::printLine(forest, ": "_el, observers, " observers"_el);
    el::io::printLine("First species: "_el, firstSpecies, "; season: "_el, season);
}

}

conf/IndividualValues/TypedCollections.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "IndividualValuesDemos.hpp"

#include <erbsland/conf/Parser.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>

namespace demo {

/// Read type-checked lists and matrices from a document.
///
/// Typed collection getters resolve the path, verify every element, and return native values. They also accept a scalar
/// of the requested type as a one-element list or matrix, which lets a setting support concise and expanded forms.
void typedCollections() {
    const auto configuration = "[survey]\n"
                               "species: \"onça-pintada\", \"tamanduá-bandeira\"\n"
                               "sample_count: 12\n"
                               "observations:\n"
                               "    * 4, 6, 3\n"
                               "    * 2, 5, 1\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);

    // Typed getters return native collections after checking every element.
    const auto species = document->getStringListOrThrow("survey.species"_el);
    const auto sampleCounts = document->getListOrThrow<int>("survey.sample_count"_el);
    const auto observations = document->getMatrixOrThrow<int>("survey.observations"_el);

    el::io::printLine("Species: "_el, species.count());
    el::io::printLine("Sample-count entries: "_el, sampleCounts.count());
    el::io::printLine("First observation: "_el, observations.valueOrThrow({}, {}));
}

}

conf/IndividualValues/TypeTests.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include "IndividualValuesDemos.hpp"

#include <erbsland/BooleanFormat.hpp>
#include <erbsland/conf/Parser.hpp>
#include <erbsland/StandardStreams.hpp>

namespace demo {

/// Inspect a value before accepting one of several representations.
///
/// The `is...()` methods make intentional alternatives explicit. Use them when an application genuinely accepts more
/// than one ELCL type; prefer a typed getter when the configuration contract requires exactly one type.
void typeTests() {
    const auto configuration = "[survey]\n"
                               "station: 42\n"
                               "reference_species: \"bugio-ruivo\"\n"_el;
    const auto document = el::conf::Parser{}.parseTextOrThrow(configuration);

    const auto station = document->valueOrThrow("survey.station"_el);
    if (station->isInteger()) {
        el::io::printLine("Numeric station: "_el, station->asIntegerOrThrow());
    } else if (station->isText()) {
        el::io::printLine("Named station: "_el, station->asTextOrThrow());
    }

    const auto referenceSpecies = document->valueOrThrow("survey.reference_species"_el);
    el::io::printLine("Reference is text: "_el, el::BooleanFormat::yesNo(), referenceSpecies->isText());
    el::io::printLine("Document is a map: "_el, el::BooleanFormat::yesNo(), document->isMap());
}

}