「識別子が見つかりませんでした」
2024年末くらいの頃のことです。自作プログラムをコンパイルしていた C++ のバージョンを C++17 から C++20 に変えたところ結構な数のコンパイルエラーが発生してしまったことがあります。
コンパイルが問題なく通っていたコードが、C++ のバージョンやコンパイラを変えたときに、なにかの判定が厳密になったりして通らなくなるということはよくあることですが、このときのコンパイルエラーの種類がちょっと尋常じゃなくて途方にくれてしまいました。
それは大量の
「識別子が見つかりませんでした」
というコンパイルエラーだったのです。
再現するとこうなります。
template <typename T>
struct BASE
{
int member;
};
template <typename T>
struct DERIVED : BASE<T>
{
void foo()
{
member = 100; // <=== ※1
}
};
見つからないのは、※1の行での member という識別子です。
今ではこれが C++20 というよりも MSVC の洗礼だということはわかります。
というわけで、どうしてこれがコンパイルが通らなくなったのか?そしてこれをどう書くのが正解なのか?を今回は深堀りしたいと思います。
ついでに、
どうも C++ のプロジェクトでテンプレートを書くのは特定の人に偏ってしまって、そうでない人はテンプレートを一切書かないという傾向があるように思います。
そういうわけで、そんな「テンプレートが怖い」な人でもテンプレーターになっていただきたく、テンプレートの基礎についても触れます。
どうぞ、最後までお読みいただければと思います。
C++03 から C++11 に切り替えたときには
以下のような型がコンパイルエラーとなりました。
std::vector<const int> a;
この型を書いた人はどういうつもりで、これを書いたのかはわかりません。
ともかく、これがコンパイルが通らなくなったと相談を受けたことがあります。
そのときは確かコンパイルエラーがMSVCの日本語のもので const な型を要素型にできないという旨だったので、それをそのまま相談者に伝えました。
std::vector<const int> のままだと要素の移動も要素の書き換えもできなくなってしまいます。
逆に、これまでどうしてこれがコンパイルできていたのかが不思議です。当時の std::vector の実装はおそらく強引に、この const をキャストなどで剥ぎ取っていたのでしょうね。
ちなみに、これを現在のMSVC C++20 でコンパイルすると
The C++ Standard forbids containers of const elements because allocator<const T> is ill-formed.
というメッセージの static_assert が失敗してしまいます。
const T というのはコンテナ型の要素(正確には allocator の)の要件を満たさない「不適格( ill-formed )」なんですね。
static_assert のメッセージが英語なのも「テンプレートが怖い」の原因の1つですね。私も英語は怖いです。
でも static_assert のメッセージはとても親切でストレートです。
テンプレートが召喚する数キロバイトの怨嗟の呪文よりかはずっとましです。
std::pair<int,int>
さて古い話になりますが、 90年代に書かれた以下のようなコードは今ではコンパイルエラーとなります。
std::map<int,int> m;
m[0] = 0;
std::pair<int,int>& r = *( m.begin() );
どうしてこれがだめなのか、わかりますか?
std::map<Key,Value>の要素型は std::pair<const Key,Value> となります。キー型には const がつくのです。
というわけで std::pair<const int,int>& を std::pair<int,int>& に代入しようとしている型の不一致によるエラーです。
なので、
std::map<int,int> m;
m[0] = 0;
std::pair<const int,int>& r = *( m.begin() );
このように書かないといけません。
コンパイルが通らないどころか、
std::map<int,int> m;
m[0] = 0;
std::pair<int,int>& r = *( m.begin() );
r.first = 100;
のようにコードでキー要素を書き換えてしまうことさえできました。
「そんなやつはおらへんやろー」
とねばっこい言い方で否定したい気持ちはわかりますが、
「これは事実談であり、こういうコードを書いた男は実在する」
のです。
誰かが書いたコードのデバッグをしていて見つけた不具合の原因が、mapのキーを書き換えたことが原因だったことがありました。
どうもキーを書き換えたら自動的にそのノードの順番も変わることを期待していたように思いますが、自動でノードの位置が変わるようなことはなく、その後の検索も正しく動かなくなります。
正解は、一度そのノードを抜いて再度変更したい値で要素を追加することです。しかし、std::map やstd::set の実装についての理解が浅い場合は、そのように書いてしまうのも無理はありません。
要素型が std::pair<const int,int> となっているのはこういう所業をできなくするためなのでしょう。
……とまあ、C++ のバージョンアップでは、このようにSTLの実装や仕様が変わったことでコードの互換性に影響することが主な要因です。
冒頭に書いた「識別子がみつかりませんでした」というメンバの宣言がなかったことになったことがちょっと尋常ではないと言った意味もわかるのではないでしょうか?
この説明をする前に、template とはどんなものか?二段階名前探索とはどういうことなのかを理解するために template の復習をしましょう。
(つづく)