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)).
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.
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 |
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.
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ó |
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_demoA 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.
using namespace std; publikus
headerben. A direktíva minden includer globális névterébe
szivárog, és ambiguitást okozhat. Javítás: távolítsuk el; a
.cpp-ben használjunk std:: minősítést vagy
szűk using std::string;-et..cpp-ben; vagy tudatos
inline.const int headerben, majd inline függvényben
használva. A konstans TU-nként külön objektum, az inline
függvény definíciói eltérő entitásra hivatkozhatnak. Javítás:
inline constexpr int..cpp névtelen namespace-ébe kerüljön..cpp-be vagy szűk hatókörbe kerüljön.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.
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.
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.
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.
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.