GCCの-Wshadowとその推移について、あとお前を消す方法

-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;
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

まあでも意図せずこれをやると困ることもあるので、-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しているとき、かつそれらが変換可能であるときだけ警告する。

個人的には、globallocalの間にもう少し何かある気がしなくもないが、あまりうまく区別できなさそうなのでよくわからなくなっている。

さて、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なのでfooxyにアクセスするには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::booleanenum 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::booleanusing 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のサポート? ええ、まだしてますよ……でももうそろそろ切ってもいいだろうか……