このページはC++23に採用された言語機能の変更を解説しています。
のちのC++規格でさらに変更される場合があるため関連項目を参照してください。
概要
C++では、スコープ([basic.scope])と名前探索([basic.lookup])に関する規格上の記述が、長年の機能追加によって分散・重複していた。 用語の定義も曖昧かつ不整合な状態となっており、特にモジュール導入後は「名前探索は同一翻訳単位内の先行宣言に限られる」という暗黙の前提が成り立たなくなり、これらの章を整理する必要性が高まっていた。
C++23では、[basic.scope]と[basic.lookup]の章がまるごと書き直され、用語と規則が整備された。 変更の大部分は規格書の編集上の整理 (editorial reorganization) であり、新たに導入された言語機能はない。 ただしこの整理の過程で、長年解決されていなかった言語の不具合報告 (CWG issue) が63件、リフレクタ上で議論されていた問題が21件、まとめて解決された。 これに伴い、いくつかの観測可能な動作 (observable behavior) の変更も含まれている。
新しく導入された用語
C++23では、スコープと名前探索を記述するための語彙が再構築された。 主な新用語や関係のある用語と、それに対応する従来用語を以下に示す。
| 新しい用語 | 概要 | 従来の用語との対応 |
|---|---|---|
| スコープ (scope) | 名前を識別するための領域 | 「declarative region」と「scope」の使い分けを廃止し、「scope」に一本化 |
| 直近スコープ (immediate scope) | プログラム上のある点を含む最小のスコープ | 暗黙的に使われていた概念を明示化 |
| 親スコープ (parent scope) | あるスコープを (それ自身を除いて) 含む最小のスコープ | 入れ子構造の記述を明確化 |
| 所属する (inhabit) | 宣言が、自身の宣言位置における直近スコープに属すること | 宣言の所在の表現を明確化 |
| 対象スコープ (target scope) | 宣言が所属するスコープ | 暗黙的に説明されていた概念を明示化 |
| 宣言位置 (locus) | 宣言がプログラム中に導入される位置 | 「point of declaration」を置き換える |
| 束縛する (bind) | 宣言が導入する名前を、対象スコープ内でその宣言と結びつけること | 名前と宣言の関係を明示化 |
| 先行する (precede) | 点Pにおいて宣言Xが「Pに先行する」という関係。Xの宣言位置がPより前にある場合、XがクラススコープにありPから到達可能 (reachable) な場合、Xが別の翻訳単位のモジュール経由でPから到達可能な場合 (export済みの宣言、もしくは同一モジュール内で内部リンケージを持たない宣言が対象。詳細な前提条件は規格を参照) のいずれかを含む | モジュール導入後の到達可能性を含めて再定義 |
| 単一探索 (single search) | あるスコープSと点Pに対して、Pに先行しSに名前Nが束縛されたすべての宣言を見つける基本操作 | 散在していた名前探索規則の最小単位として整理 |
| 探索文脈 (lookup context) | メンバ修飾名 (./->の右側の名前) では、関連オブジェクト式の型。それ以外の修飾名では、先行するnested-name-specifierが指し示す型・テンプレート・名前空間 |
修飾名探索の起点を明示化 |
| コンポーネント名 (component name) | unqualified-idを構成する根本の名前 (template-idの場合はそのテンプレート名)。テンプレート引数やdecltypeの中には再帰しない |
「型として現れる名前」という曖昧な表現を置き換える |
| 修飾名 (qualified name) / メンバ修飾名 (member-qualified name) | qualified-idやusing宣言子の終端の名前を「修飾名」、./->の右側の名前を「メンバ修飾名」と呼ぶ |
名前探索の対象となる名前の種類を整理 |
| 指名可能 (nominable) | クラス・クラステンプレート・名前空間Eに対して、点Pにおいて宣言が「指名可能」とは: (1)宣言がPに先行し、(2)宣言がブロックスコープに所属しておらず、(3)宣言の対象スコープがEのスコープか、Eが名前空間のときはそのインライン名前空間集合の任意要素のスコープと一致する、こと | 修飾名探索での可視性を整理 |
| 対応する (correspond) | 2つの宣言が、同じ名前を (再) 導入するか、両方ともコンストラクタを宣言するか、両方ともデストラクタを宣言する関係 (一定の例外を除く) | 宣言マッチング規則を統一的に表現 |
| 潜在的に衝突する (potentially conflict) | 対応する宣言が、共有された名前を異なるエンティティに対して指す可能性があること | 宣言間の衝突条件を明確化 |
観測可能な動作の変更
この整理の主目的は規格の記述の整理であるが、その過程でCWG issueの解決を兼ねた、いくつかの観測可能な動作の変更が含まれている。
using宣言の参照先による置換
非依存なusing宣言は、名前探索の段階で「using宣言そのもの」ではなく「その参照先の宣言」に置き換えられて扱われるようになった。
これにより、usingディレクティブと組み合わせたときの曖昧さの判定が、規格上明確に規定される。
namespace F { float x; }
namespace A {
namespace I { int x; }
using namespace I;
using F::x;
void f() {
x = 0; // エラー: 曖昧 (Aの中のF::xと、using namespace I経由で見えるI::xが衝突)
A::x = 1; // OK: A内の修飾名探索ではF::xのみが見つかる
}
}
friend宣言における名前探索範囲の制限 (CWG2370)
friend宣言の宣言子IDの中に現れる名前は、まず宣言子IDが指す名前空間の直近 (immediate) スコープのみで探索される。
そのスコープに見つからなければ、通常の非修飾名探索にフォールバックする。
直近スコープには、内部の (inline 含む) 入れ子の名前空間で導入された名前は含まれない。
using D = double;
namespace A {
inline namespace N { using C = char; }
using F = float;
void f(D);
void f(C);
void f(F);
}
namespace B {
class Y {
friend void A::f(D); // OK: Aの直近スコープにDはないが、非修飾名探索で外側のD=doubleが見つかる
friend void A::f(F); // OK: Aの直近スコープでF=floatが見つかる
friend void A::f(C); // エラー: CはAの内部の inline namespace Nに属しており、Aの直近スコープには含まれない
};
}
デストラクタ名の探索
デストラクタ呼び出しの~XのXは、注入されたクラス名 (injected-class-name) として解決される必要がある。
他のクラスを経由するtypedef名などは候補として考慮されない。
struct A { using X = A; };
struct B : A { struct X {}; };
void f(B* b) {
b->A::~X(); // エラー: A::X (typedef名) はデストラクタ名の探索で考慮されない
}
テンプレートパラメータスコープの位置づけ
テンプレートパラメータスコープの親となるのは、別のテンプレートパラメータスコープのみであると規定された。 これにより、テンプレート定義内の非修飾名探索における探索順序が明確になる。 ただしCWG459 (基底クラスはテンプレートパラメータより先に探索される) の振る舞いは維持される。
依存名としての修飾名の拡張
依存型を含む変換関数ID (conversion-function-id) も依存名として扱われるよう、依存名の定義が拡張された (CWG1500・CWG1936の解決)。 これに伴い、依存名の探索は型の置換後に行われることが明確化された。
モジュールをまたぐ名前探索
別の翻訳単位の宣言であっても、exportされた宣言や、同一モジュール内で内部リンケージを持たない宣言であってモジュール経由で到達可能なものは、名前探索の対象となることが規格上明示された。
これはモジュール機能の前提に合わせた整理である。
単集合に対するオーバーロード集合の取り扱い
オーバーロード集合は、要素が1つしかない場合 (singleton) でも一貫した規則で構築されるようになった。
この機能が必要になった背景・経緯
[basic.scope]および[basic.lookup]の問題は次のように整理できる:
- 「scope」の定義の二重性: 規格上、「scope」という語が異なる意味で2通り定義されており、暗黙のうちに名前探索の規則を含んでいたが、[basic.lookup]の記述と完全には一致していなかった。
- 名前空間の取り扱いの不整合: グローバル名前空間を含む名前空間が、ある場所では1つのdeclarative regionとして、別の場所では複数のregionとして記述されていた。
- モジュール導入による前提の崩れ: 「名前探索は同じ翻訳単位の中の、その時点までに現れた宣言だけを対象とする」という暗黙の前提が、モジュールの導入で成り立たなくなった。
<トークンの曖昧性: テンプレートIDか比較演算子かを判別するための規則が、名前探索の記述と絡み合っていた。- 同一エンティティの判定: 同じ名前の複数の宣言が同一のエンティティを導入するか、別のエンティティを導入するかを一貫した規則で判別できなかった。
C++23では、これらの問題を解決するために、新しい用語を導入したうえで、[basic.scope]と[basic.lookup]の章が全面的に書き直された。 63件のCWG issueと21件のリフレクタ議論で報告された問題が、この整理によって一括で解決された。
ユーザーが書く通常のコードに与える影響は限定的だが、規格書を直接参照するC++ユーザー、コンパイラ実装者、ライブラリ実装者にとっては、本変更でC++の名前探索規則が初めて一貫した枠組みのもとに整理されたことになる。
編集上の主な変更点
[basic.scope]と[basic.lookup]の章は全面的に書き直された。 編集上の主な変更点としては、以下が挙げられる:
- 「declarative region」の語が廃され、「scope」に統一された。
- 「point of declaration」の語が廃され、「locus」に置き換えられた。
- 「in scope」「global function」「global object」「overloaded function」といった曖昧な表現が削除された。
- 散在していた名前探索規則が、単一探索 (single search) などの基本操作の組み合わせとして再定義された。
- 名前探索は常に「名前」に対する操作として定義され、id-expressionやtype-idに対する操作とは区別されるようになった。
関連項目
参照
- P1787R6 Declarations and where to find them
- CWG2370 friend declarations of namespace-scope functions
- CWG1500 Name lookup of dependent conversion function
- CWG1936 Dependent qualified-ids
- そのほか本提案で解決された計63件のCWG issue・21件のリフレクタ議論 (P1787R6本文を参照)