C++のill formedなスコープ解決子用法
C++に関して、言語的に正しいけれどもill formedな書き方でトラブルに見舞われたので記しておきます。
再現プログラム
言葉で説明すると非常に面倒なので、まず問題を再現するプログラムを紹介します。
#include <stdint.h>
class foo {
public:
uint32_t bar(unsigned int a);
int baz( int a);
uint32_t qux(uint32_t a);
};
// A
uint32_t ::foo::bar(unsigned int a) { return a; }
// B
int ::foo::baz( int a) { return a; }
// C
uint32_t foo::qux(uint32_t a) { return a; }
int main() {
foo my_foo;
return (my_foo.bar(0));
}
ここで、Aはill formedで、B,Cは問題ない書き方です。Compiler Explorerで各種のコンパイラがどんな反応をするか見てみましょう。
GCCはコンパイルできますが、Clangはエラーを返します。以下はClangのエラーメッセージです。
<source>:11:1: error: 'uint32_t' (aka 'unsigned int') is not a class, namespace, or enumeration
11 | uint32_t ::foo::bar(unsigned int a) { return a; }
| ^
1 error generated.
Compiler returned: 1
この件は、LLVMプロジェクトにIssue 108655として報告したのですが、コンパイラには問題はなくIll Formedだという事でした。
何がIll formedなのか
そもそものこの問題ですが、X::Y::Z という字句の並びがプログラム上に現れたとき、コンパイラが頭から1字句先読み解析してこれをバックトラックなしに処理できないという言語仕様上の問題があります。
ここでXが予約後の場合、X::Yという名前空間の表記はC++言語仕様にはありません。ですので、コンパイラは予約語Xに続いて ::Yという名前空間を解析します。これは、関数定義に現れる書き方です。
int ::foo::bar();
一方で、Xが識別子の場合、Xが名前空間を持つ識別子なのかそうでないかでその後の処理が大きく変わります。しかしそれは意味解析が必要と言う事であり、構文解析だけでは先に進めません。
bar ::foo::bar();
uintはヘッダーファイルの中でtypdefで宣言された識別子であり、予約語ではありません。このため、バックトラックを使わずに1字句先読み構文解析で構文を決めることができません。LLVMがエラーとしているのはこのためです。
先に挙げたエラーメッセージは最初の字句が「クラスでも、名前空間でも列挙型でもない」と言っており、構文解析は名前空間として処理したけれど意味解析で矛盾が生じたのだろうと推測されます。
ワークアラウンド
これは言語仕様上に起因する問題であり、入れ子のスコープに関する曖昧さとしてC++言語仕様のワーキンググループにも認識されているようです。
問題を避けるには関数定義部でグローバルな"::"スコープ解決子を使わないようにします。
// uint32_t ::foo::bar(unsigned int a) { return a; }
uint32_t foo::bar(unsigned int a) { return a; }
Discussion