-Wshadow
は-Wall
でオンにならない警告オプションで、shadowingを警告してくれる。
そもそも、実はC++にもshadowingというのはあり、ブロックを分けさえすれば同じ名前の変数を定義しても特に問題はない。ブロックが同じだとエラーになるのであまり便利ではないが。
#include <iostream> int main() { int x = 42; std::cout << x << std::endl; { double x = 3.14; std::cout << x << std::endl; } return 0; }
まあでも意図せずこれをやると困ることもあるので、-Wshadow
というオプションによって警告することができる。
prog.cc: In function 'int main()': prog.cc:9:16: warning: declaration of 'x' shadows a previous local [-Wshadow] 9 | double x = 3.14; | ^ prog.cc:5:9: note: shadowed declaration is here 5 | int x = 42; | ^
これには三つのオプションがさらにあり、global
, local
, compatible-local
となっている。この順に少しずつ弱くなる。
global
はありとあらゆるshadowingを警告する。これはあとでいくつかまとめて例を出すつもりだ。
local
は、ローカル変数がローカル変数をshadowingしているとき(一番最初の例だ)に警告する。
compatible-local
は、ローカル変数がローカル変数をshadowingしているとき、かつそれらが変換可能であるときだけ警告する。
個人的には、global
とlocal
の間にもう少し何かある気がしなくもないが、あまりうまく区別できなさそうなのでよくわからなくなっている。
さて、global
だけではまずかったのか、どういうときに人類はお目こぼしを貰いたいのだ? ということが次の疑問だ。
例えば、global
はグローバルにあるusing
による型エイリアスと、関数の引数の名前が衝突していたりすると警告する。
#include <iostream> using x = int; double f(double x) {return x * 2;} int main() { std::cout << f(3.14) << std::endl; return 0; }
prog.cc: In function 'double f(double)': prog.cc:5:17: warning: declaration of 'x' shadows a global declaration [-Wshadow] 5 | double f(double x) {return x * 2;} | ~~~~~~~^ prog.cc:3:7: note: shadowed declaration is here 3 | using x = int; | ^
他に、例えば以下の人は「globalな変数やエイリアスとenum class
の名前が警告される」と文句を言っている。
c++ - -Wshadow=global considers enum class entry shadowing a global. Why? - Stack Overflow
つまりこういうことだ。
#include <iostream> inline constexpr int x = 42; enum class foo { x = 0, y = 1 }; int main() { std::cout << x << std::endl; return 0; }
prog.cc:7:5: warning: declaration of 'x' shadows a global declaration [-Wshadow] 7 | x = 0, | ^ prog.cc:3:22: note: shadowed declaration is here 3 | inline constexpr int x = 42; | ^
ここで注意したいのは、これがenum
ではなくenum class
であることだ。2021年にこんなことを注意する必要はないと思うが、enum class
なのでfoo
のx
やy
にアクセスするにはfoo::x
と明示的に書く必要がある。なので実際にはここで混乱は生じない。
これはどうも、Stackoverflowで質問する人が出てくる程度には、厳しすぎると思われていたのだろう。ローカル変数のshadowingだけをチェックさせたい人のために、=local
と=compatible-local
が存在するというわけだ。
この-Wshadow=hoge
のオプションは、gcc-7で導入された。これ以前のバージョンでは、-Wshadow=global
といった制御は効かない。
GCC 7 Release Series — Changes, New Features, and Fixes - GNU Project
ついでに-Wshadow
自体はいつ導入されたのかと思って、6、5、4と遡ってみたが、changesの中にshadow
は入っていなかった。てことは結構昔からある機能なのかな。
こういうオプションごとの年表みたいなのが欲しいと結構昔から思っているが、結構作業が膨大になりそうなので手を出せずにいる。せめて、ということでこうして調べてものだけ備忘録を残している。
ところでなぜ調べたのか? いつものことだが、toml11に「-Wshadow
で警告が出る」との報告がきたからだ。世の中には私の想像よりもたくさん、-Wall -Wextra
ではオンにならない警告オプションを大量に手動で設定し、-Werror
を付けてコンパイルしている人がいるらしい。
原因の一つは引数の名前をkey
にしていたからで、これがtoml::key
という型エイリアスと衝突していた。これはいい。直せる。
もう一つは、型エイリアスtoml::boolean
とenum class value_t
の名前、toml::value_t::boolean
が衝突しているというものだ。これはAPIに深く刻まれているので修正とかそういうのはできそうにない。
一応念のため、これが衝突しているからと言って問題が起きるのは限られたケースでしかないと主張しておく。そもそもenum class
の列挙子は値であり、toml::boolean
は型だ。コンテキストからこれらの区別がつかなくなることはほぼない(非型テンプレート引数を取るメタ関数と型名を取るメタ関数を別の名前空間で同名で定義した後にそれぞれをusing namespace
すると衝突するだろうか?)。また、通常、enum class
の列挙子にアクセスするにはvalue_t::boolean
のようにする必要があり、これを衝突させるにはusing enum
というC++20での新機能を使用する必要がある。その上で、toml::boolean
をあいまいにするためにusing namespace toml
あるいはtoml
名前空間の中でことを起こす必要がある。ちなみにこれらを同時に使っても(using namespace toml; using enum toml::value_t;
)g++-11では衝突による問題は起きず、問題を起こそうとするとusing enum toml::value_t
ではなくusing toml::value_t::boolean
とusing toml::boolean
を同じスコープで両方使う必要がある。ここまで明示的に書いて問題を起こすのはもう故意ではないのか。これはfalse positiveだと言って過言ではあるまい。
というわけで、「お前を消す方法」の出番だ。主要コンパイラは、コード内の特定の箇所においてコンパイラオプションをコード側から制御する方法を持っている。コードを読んでいるとたま〜にでくわすことがある。
Diagnostic Pragmas (Using the GNU Compiler Collection (GCC))
// その時点でのオプション状態の情報をコンパイラの内部スタックにpush #pragma GCC diagnostic push // オプション状態をいじる(-Wshadowingを無視) #pragma GCC diagnostic ignored "-Wshadow" // この範囲のコードは`-Wshadow`の影響を受けない // 前にpushしたオプション状態に戻る。 // ちなみに何もpushしていないと、コマンドライン引数で与えられた状態に戻るらしい。 #pragma GCC diagnostic pop
これによって様々な違法行為をしつつ表面上は質の高いコードを保っているかのような悪事が可能になる。
とはいえ、「プログラマがここ(だけ)は大丈夫であるということを示す」という意味で、これはRustのunsafe
に近いものだろう。むしろ、「このコードで-Wshadow
を付けないでください」と言うよりも、「ここだけどうにもならないけれど、そこ以外は-Wshadow
で検査してくれて大丈夫です」の方が安全ですらある。のでこれは使い方によってはいいものだ。ところで、一般的にあらゆる道具は使い方によってはいいものである。
そういうわけで、ここはどうしようもないというところではpragma GCC diagnostic ignored
を使い、あとでpop
して元の状態に復帰させておこう。
ちなみにこれをいろいろなコンパイラで使う方法は以下の記事にまとめられている。
https://www.fluentcpp.com/2019/08/30/how-to-disable-a-warning-in-cpp/
あと、-Wshadow
とこの辺の挙動が両方gcc 4.8とgcc 5以降で結構違ったので困った。えっgcc 4.8のサポート? ええ、まだしてますよ……でももうそろそろ切ってもいいだろうか……