19.2 Namespace, header és névfeloldás

A C-ben a legtöbb könyvtár a globális névtérben él, ezért két független könyvtár azonos nevű függvénye vagy változója linkeléskor ütközhet; a hiba gyakran csak a végső program összeállításakor jelentkezik. A C++ namespace nyelvi eszköze ezt a problémát már a forrás szintjén kezeli: a neveket névterekbe szervezi, a minősített név pedig egyértelművé teszi, melyik entitásra hivatkozunk. Ez az alfejezet többmodulos könyvtár példáján mutatja be a névütközések elkerülését, a header- és implementációszervezést, valamint a linkage és az ODR előképét. Az előfeltétel a C++ programmodell (C++ programmodell és zero-overhead elv (19.1)), a C header-higiénia (Headerek, deklarációk és include-higiénia (2.2)) és a szimbólumok/linkage alapjai (Szimbólumok, linkage és relocation (3.1)).

19.2.1 Namespace, beágyazott namespace és alias

A namespace deklaratív régió, amely neveket fog össze. A benne deklarált nevek a namespace tagjai; kívülről minősített névvel (acme::diag::log) vagy a név bevitelét engedélyező using-deklarációval érhetők el. A minősítetlen névfeloldás a mindenkori hatókörből kifelé haladva keresi a nevet; a minősített névfeloldás csak a megnevezett névtérben és annak inline namespace-eiben keres. A függvényhívásoknál ehhez még argumentumfüggő névfeloldás (argument-dependent lookup, ADL) is társulhat, amely a függvényargumentumok típusainak névtereit is figyelembe veszi; ennek részletei később, a template-es fejezetben kerülnek elő.

A beágyazott namespace definíciója C++17 óta tömören írható:

namespace acme::diag {   // C++17: egyenértékű a beágyazott namespace-ekkel
int version_major = 1;
}

A namespace újranyitható: ugyanazt a névteret több headerben és .cpp-ben is folytathatjuk, ami a moduláris szervezés alapja. Az inline namespace a verziózás eszköze: tagjai úgy látszanak, mintha a befoglaló névtérben lennének deklaráva, miközben a minősített név megőrzi a verziót. A névtér-alias (namespace d = acme::diag;) alternatív nevet vezet be; az alias attól a ponttól érvényes, ahol deklaráljuk, és nem nyitható újra namespace-ként. Publikus headerben az alias is névimport, ezért inkább a .cpp-ben vagy szűk hatókörben használjuk.

Fontos megkülönböztetni a namespace detail konvenciót a nyelvi védelemtől: a detail név semmilyen hozzáférés-védelmet nem ad, csak jelzi, hogy a felhasználónak nem ajánlott rá támaszkodnia. A valódi belső implementációs részleteket névtelen namespace-be vagy külön, nem publikált headerbe érdemes tenni.

19.2.2 using deklaráció és using direktíva

A két eszköz szemantikailag különbözik, és a header-higiénia szempontjából különböző kockázatot hordoz.

A using-deklaráció (using acme::diag::log;) egy nevet visz be a jelenlegi deklarációs régióba. Függvénynév esetén a cél-névtérben látható túlterhelések is részt vesznek a további túlterhelés-feloldásban. A bevitel a deklaráció helyétől a hatókör végéig tart. Ha ugyanabban a hatókörben már van azonos nevű, összeférhetetlen entitás, a program hibás; ezért a using-deklarációt lehetőleg szűk, helyi hatókörben alkalmazzuk.

A using-direktíva (using namespace acme::diag;) nem visz be nevet a hatókörbe. A minősítetlen névfeloldás során a megnevezett névtér nevei úgy látszanak, mintha a direktívát és a névteret közrefogó legközelebbi névtérben deklarálták volna őket. Emiatt a direktíva hatása nem lokális: egy headerben elhelyezett using namespace std; minden olyan fordítási egység globális névterébe „beszivárog”, amelyik include-olja a headert, és minden későbbi minősítetlen névfeloldást befolyásol. Ez váratlan ambiguitásokat okozhat, különösen ADL-lel kombinálva, és megváltoztathatja a felhasználó kódjának jelentését.

Gyakorlati szabály: publikus headerben namespace-scope-on ne legyen using namespace. A using-deklaráció is lehet névimport, ezért headerben csak indokolt esetben, szűk névre használjuk; a .cpp-ben vagy függvényhatókörben viszont elfogadható, ha javítja az olvashatóságot. A using namespace std; még .cpp-ben is kerülendő nagy fájlokban; helyette std:: minősítés vagy szűk using std::string; használható.

Forma Nyelvi hatás Headerben
using N::f; nevet visz be a jelenlegi régióba lehetőség szerint kerülendő namespace-scope-on
using namespace N; nem visz be nevet; a minősítetlen lookup kiterjesztése ne használjuk
namespace a = N; alternatív név csak indokolt esetben, inkább .cpp-ben

19.2.3 Header és implementáció

A header a szerződést tartalmazza: típusdeklarációkat, függvényprototípusokat, osztálydefiníciókat, template-eket, inline függvényeket és inline változókat. A nem inline függvények és változók definíciója a .cpp-be kerül, ahol a nevet a namespace minősítésével definiáljuk. A header védelmét include guard vagy #pragma once biztosítja; a „include what you use” elv szerint minden használt típushoz a megfelelő header-t include-oljuk, és a header ne támaszkodjon arra, hogy a felhasználó „úgyis” include-ol valamit.

A névtelen namespace a fordítási egységen belüli segédfüggvények és -típusok elhelyezésére való. A benne deklarált nevek belső linkage-t kapnak, és minden fordítási egységben külön entitást jelölnek. A .cpp-ben ez kiválóan alkalmas arra, hogy a publikus API-t ne szennyezzük segédfüggvényekkel. Headerbe viszont nem való: minden includer külön példányt kapna, ami bloat-hoz, meglepő cím-összehasonlításokhoz és – ha inline függvény vagy template használja – ODR-problémákhoz vezethet.

19.2.4 const, inline változó és az ODR előképe

A namespace-scope változók linkage-je nem mindig external. A const (és így a constexpr) változó alapértelmezésben belső linkage-t kap, ha nincs extern-ként deklarálva és nem inline. Ezért a headerben definiált const int max_retries = 3; minden fordítási egységben külön objektum; értékként használva általában működik, de címe TU-nként eltérhet, és ha egy inline függvény hivatkozik rá, az inline függvény definíciói különböző entitásokat neveznek meg – ez ODR-sértés, akár diagnosztika nélkül is.

A C++17 inline változója ezt oldja meg: inline constexpr int max_retries = 3; external linkage-ű, egyetlen entitás a programban, és több fordítási egységben is definiálható, feltéve, hogy a definíciók azonosak. Ugyanez az elv érvényes az inline függvényekre is.

Az egységes definíció szabálya (One Definition Rule, ODR) előképe: egy nem inline függvénynek vagy változónak az egész programban pontosan egy definíciója lehet; egy inline függvénynek vagy változónak több TU-ban is lehet definíciója, de ezeknek azonosnak kell lenniük, és ugyanazokat az entitásokat kell megnevezniük. Az osztálydefiníciók több TU-ban is szerepelhetnek, ha azonosak. A szabály megsértése gyakran „ill-formed, no diagnostic required” (NDR): a fordító nem köteles jelezni, a program viselkedése pedig nem definiált.

Deklaráció namespace scope-on Linkage Egyediség
int g; external egy definíció a programban
static int g; internal TU-nként külön objektum
namespace { int g; } internal TU-nként külön objektum; típusokra is jó
const int c = 1; internal TU-nként külön objektum
inline constexpr int c = 1; external egy inline entitás, több azonos definíció

19.2.5 Kidolgozott példa: többmodulos diagnosztikai könyvtár

A következő példa egy publikus headerből, egy implementációs .cpp-ből és egy main.cpp-ből áll. A publikus API a acme::diag névtérben él; a .cpp-ben névtelen namespace tartalmazza a segédfüggvényt, a fejlécben pedig inline constexpr biztosítja a verzióállandó egyetlenségét.

// include/acme/diag.hpp
#ifndef ACME_DIAG_HPP
#define ACME_DIAG_HPP

#include <string>
#include <string_view>

namespace acme::diag {

inline constexpr int version_major = 1;

enum class level { info, warning, error };

void log(level lvl, std::string_view message);
std::string format(level lvl, std::string_view message);

namespace detail {
std::string_view level_name(level lvl);
} // namespace detail

} // namespace acme::diag

#endif
// src/diag.cpp
#include "acme/diag.hpp"

#include <cstdio>
#include <string>

namespace {
// TU-local helper: belső linkage, a könyvtár felhasználója nem látja.
std::string bracketed(std::string_view s) {
    std::string out;
    out.reserve(s.size() + 2);
    out.push_back('[');
    out.append(s.data(), s.size());
    out.push_back(']');
    return out;
}
} // anonymous namespace

namespace acme::diag::detail {

std::string_view level_name(level lvl) {
    switch (lvl) {
    case level::info:    return "INFO";
    case level::warning: return "WARN";
    case level::error:   return "ERROR";
    }
    return "?";
}

} // namespace acme::diag::detail

namespace acme::diag {

std::string format(level lvl, std::string_view message) {
    std::string out;
    const auto name = detail::level_name(lvl);
    out.reserve(name.size() + message.size() + 2);
    out.append(name);
    out.append(": ");
    out.append(message.data(), message.size());
    return out;
}

void log(level lvl, std::string_view message) {
    const auto line = format(lvl, message);
    std::fputs(bracketed(line).c_str(), stderr);
    std::fputc('\n', stderr);
}

} // namespace acme::diag
// src/main.cpp
#include "acme/diag.hpp"

#include <string>

int main() {
    using acme::diag::level; // szűk using-deklaráció, csak függvényhatókörben
    acme::diag::log(level::info, "start");
    const std::string s = acme::diag::format(level::warning, "disk almost full");
    acme::diag::log(level::warning, s);
}

Fordítás és futtatás a tanuló laborjában:

g++ -std=c++20 -Wall -Wextra -Wpedantic -Iinclude src/diag.cpp src/main.cpp -o diag_demo
./diag_demo

A tervezett kimenet: [INFO: start], majd [WARN: disk almost full]. A példa C++20 nyelvi szabályokra épül; a kimenet tervezett, nem mért eredmény, és a kód fordítását/futtatását itt nem állítjuk megtörténtnek.

19.2.6 Tipikus hibák és javításuk

19.2.7 Összefoglaló

A namespace a C++ névütközés-kezelésének alapja: a publikus API-t érdemes saját, dokumentált névtérbe szervezni, a belső segédeszközöket pedig névtelen namespace-be tenni a .cpp-ben. A using-deklaráció nevet visz be, a using-direktíva csak a minősítetlen névfeloldást terjeszti ki; utóbbi headerben különösen veszélyes. A header a szerződést, a .cpp a definíciókat tartalmazza. A const/constexpr változók alapértelmezett belső linkage-je és az inline változók external linkage-je az ODR szempontjából kulcsfontosságú: headerben használt állandóhoz inline constexpr kell. A C extern "C" határ kezelése későbbi rész témája.

19.2.8 Feladatok

  1. Alap – namespace-be szervezés. Alakítsd át a következő C-szerű API-t úgy, hogy a acme::diag névtérbe kerüljön, és a main minősített névvel hívja: void log_msg(int level, const char* msg);, int log_level;. Írd le, hogyan változik a hívás, és miért nem kell a log_msg nevet átnevezni.

  2. Közepes – header-javítás. Adott egy header: #pragma once, #include <string>, using namespace std;, int counter = 0;, void bump() { ++counter; }. Sorolj fel legalább három problémát, és javítsd a headert úgy, hogy több TU-ból is include-olható legyen. Indokold a linkage-változtatásokat.

  3. Haladó – ODR-diagnosztika. Egy headerben ez áll: const int max_retries = 3; és inline bool retryable(int n) { return n < max_retries; }. Két .cpp include-olja a headert, és mindkettő használja a retryable függvényt. Mi a probléma? Javítsd a kódot, és fogalmazd meg, milyen ellenőrzéssel lehetne kimutatni a hibát.

19.2.9 Megoldások

1. megoldás. A névtérbe szervezés:

namespace acme::diag {
void log_msg(int level, const char* msg);
extern int log_level; // deklaráció; a definíció valamelyik .cpp-ben
}

A hívás acme::diag::log_msg(level, "start"); lesz. A függvény nevét nem kell megváltoztatni, mert a namespace már egyedivé teszi a teljes nevet; a linker szintjén a minősített név szerepel. A log_level esetében a headerben csak extern deklaráció állhat, a definíció a .cpp-be kerül. Ellenőrzés: a header önmagában is fordítható legyen, és két különböző könyvtár azonos nevű log_msg-je ne ütközzön, ha külön namespace-ben van.

2. megoldás. Problémák: a using namespace std; minden includer globális névterébe szivárog; a counter nem inline, ezért több TU-ból include-olva többszörös definíció lesz; a bump nem inline, ezért szintén többszörös definíció lesz; a header nem biztosítja, hogy a counter egyetlen entitás legyen. Javítás:

#pragma once
#include <string>

namespace acme::diag {
inline int counter = 0; // C++17: egy inline entitás
inline void bump() { ++counter; }
}

A using namespace std; eltávolítandó; ha kell, a .cpp-ben using std::string; használható. Ellenőrzés: két TU-ból include-olva és linkelve ne legyen „multiple definition” hiba; a counter címe a programban minden TU-ban ugyanaz legyen.

3. megoldás. A const int max_retries = 3; namespace-scope-on belső linkage-ű, mert nem extern és nem inline. Minden TU külön max_retries objektumot kap. Az inline bool retryable(int) external linkage-ű inline függvény, amelynek minden TU-beli definíciója a saját TU-jának max_retries objektumára hivatkozik. Ezért az inline függvény definíciói nem ugyanazt az entitást nevezik meg, ami ODR-sértés; a program ill-formed, diagnosztika nélkül is. Javítás:

inline constexpr int max_retries = 3;
inline bool retryable(int n) { return n < max_retries; }

Az inline constexpr external linkage-ű, egyetlen entitás a programban. Ellenőrzés: két TU-ból is include-olva linkeljük a programot, és hasonlítsuk össze a &max_retries címét a két TU-ban – egyezniük kell. Ez a cím-összehasonlítás csak ellenőrző eszköz, nem a program logikájának része.