Paths for Referring to Items in the Module Tree
English

Paths for Referring to Items in the Module Tree

The previous section reached every item through a path: mathlib::sum_of_squares, mathlib::trig::sin_deg, stats::sum_of_squares_twice. Each was written out in full, at the call site, every time. This section looks at what a path actually is, how a module’s own dotted name shapes it, and how little shorthand scpp gives you for skipping any of it.

A path is nothing more than an item’s namespace nesting, joined with ::, ending in its own name. It does not depend on which file declares the item, and it is a separate thing from the module’s own dotted import name – the two use different separators for a reason, as the first section below shows.

Every example below lives in one package.

scpp.toml:

manifest-version = 1

[package]
name = "mathlib-app"
version = "0.1.0"

[[bin]]
name = "app"
sources = ["src/*.scpp"]
scpp build
./.scpp/build/*/dev/mathlib-app/app

A module’s dotted name maps segment for segment onto a path

Control Scope and Privacy with Modules mentioned in passing that a module’s own name can have several dot-separated segments, such as mathlib.trig, and that each segment maps one-for-one onto a ::-separated namespace segment. Every module up to this point used a single-segment name, so this never came up directly. A real, two-segment module name makes it concrete.

src/trig.scpp:

export module mathlib.trig;

namespace mathlib::trig {
    export int sin_deg(int x) {
        return x;
    }
}

src/main.scpp:

import std;
import mathlib.trig;

int main() {
    std::println("{}", mathlib::trig::sin_deg(30));
    return 0;
}

Output:

30

import mathlib.trig; names the module the way it names itself, with a dot. Reaching what it exports still uses ::, exactly as before – mathlib::trig::sin_deg. The module’s own two dot-separated segments, mathlib and trig, become the two required ::-separated namespace segments, mathlib and trig. This is the same namespace-matching rule from the previous section, only the module’s own name now has more than one segment for it to match.

The rule still holds exactly: a namespace one segment short of the module’s own name is rejected, the same way a missing namespace was rejected before.

export module mathlib.trig;

namespace mathlib {
    export int sin_deg(int x) {
        return x;
    }
}

Compiler output:

src/trig.scpp:4:12: error: exported function 'mathlib::sin_deg' must be declared in namespace matching module 'mathlib.trig' -- ch11 §11.5
 4 |     export int sin_deg(int x) {
   |            ^

namespace mathlib only supplies the first of the two required segments, so it is treated exactly like any other namespace that fails to match: the same error as an unrelated namespace, or no namespace at all.

Only a name declared in that same exact namespace can skip its path

A namespace can still nest deeper than a module’s own name requires – that part of the rule from the previous section is unchanged. What is new here is what an unqualified call can and cannot reach once namespaces start nesting.

src/trig.scpp, with a helper and a further-nested namespace added:

export module mathlib.trig;

namespace mathlib::trig {
    int double_it(int x) {
        return x * 2;
    }

    export int sin_deg(int x) {
        return double_it(x);
    }
}

namespace mathlib::trig::deg {
    export int right_angle() {
        return mathlib::trig::double_it(45);
    }
}

src/main.scpp:

import std;
import mathlib.trig;

int main() {
    std::println("{}", mathlib::trig::sin_deg(400));
    std::println("{}", mathlib::trig::deg::right_angle());
    return 0;
}

Output:

800
90

sin_deg calls double_it with no path at all, and that works, because both are declared directly inside the same namespace mathlib::trig { ... } block. right_angle, declared one level deeper in mathlib::trig::deg, calls the same double_it through its full path, mathlib::trig::double_it, even though mathlib::trig is its own directly enclosing namespace. Dropping that path and calling double_it bare from inside mathlib::trig::deg does not work:

namespace mathlib::trig::deg {
    export int right_angle() {
        return double_it(45);
    }
}

Compiler output:

src/trig.scpp:15:16: error: call to unknown function 'double_it'
 15 |         return double_it(45);
    |                ^

An unqualified call only reaches a name declared directly in that exact same namespace (or, as every earlier example’s plain functions showed, declared in no namespace at all). It does not climb outward through enclosing namespaces the way looking up a variable in nested blocks does. Crossing into any other namespace – even the one directly wrapping the namespace you are writing in right now – always means writing out the full path.

A leading :: starts the search at the outermost scope

A path can also begin with a leading ::. This does not change what a fully written-out path like mathlib::trig::sin_deg reaches – it still names the same item – but it does guarantee that the search starts from the outermost scope rather than considering anything else already in scope at the call site. The difference only shows up when something else in scope could otherwise be reached by the same bare name.

import std;

int count() {
    return 100;
}

int main() {
    int count = 7;
    std::println("{}", count);
    std::println("{}", ::count());
    return 0;
}

Output:

7
100

Both counts are visible inside main: a local variable and, without the leading ::, an ordinary function of the same name would simply be shadowed by it. Writing count bare reaches the local variable. Writing ::count() skips straight past it and reaches the function declared at the outermost scope. The same leading :: works in front of a full path, with the same meaning – start at the outermost scope, then follow the rest of the path exactly as written:

import std;
import mathlib.trig;

int main() {
    std::println("{}", ::mathlib::trig::sin_deg(400));
    std::println("{}", ::mathlib::trig::deg::right_angle());
    return 0;
}

Output:

800
90

Nothing in scope at main’s call sites could have been confused with mathlib::trig::sin_deg or mathlib::trig::deg::right_angle here, so the leading :: makes no difference to the result. It is available on any path, not only ones that happen to need it.

A path still cannot reach what was never exported

None of this reopens the privacy rules from the previous section. A path that exactly matches a declaration’s own namespace nesting still fails if that declaration was never export-ed – writing the path correctly is not enough on its own.

import std;
import mathlib.trig;

int main() {
    return mathlib::trig::double_it(5);
}

Compiler output:

src/main.scpp:5:12: error: call to unknown function 'mathlib::trig::double_it'
 5 |     return mathlib::trig::double_it(5);
   |            ^

double_it really does live at mathlib::trig::double_it – that is the exact path used inside trig.scpp itself in the section above – but it was never exported, so no path reaches it from main.scpp. A path only ever describes where something lives; whether it can be followed from outside the module still depends entirely on export.

The path rules so far

Every path in this section was still written out by hand, in full, in every file that needed it. The next section returns to import itself, and to how it and a path’s own qualified name work together in practice.


← Previous: Control Scope and Privacy with Modules · Table of Contents