Wshadowがenum classとの衝突に文句を言う場合と言わない場合

今回は短めです。流石に学習しました。


そういえば以前toml11 v3で発生していた-Wshadowの警告が特に意識しないうちに消えているな~と思って、確認していました。

以前出ていた警告は、以下のコードが原因の警告でした。enum classと型エイリアスが衝突しているという話です。

namespace toml
{
using boolean = bool;
enum class value_t
{
    boolean = 0,
    // ...
}
} // toml

value_tenum classなので、これが実際に曖昧になるのはC++20でusing enum toml::value_t;をしたうえでusing namespace tomlをした場合だけのような気がしますが(省略しすぎでは?)、それでも警告は出ます。

今のtoml11 v4では値の型が可変になったのでbooleanのような型エイリアスがなくなり、原因コード自体が消えたために警告も消えているわけですが、再発防止のために条件をちゃんと知っておこうと考えました。 ちょうど別のenumを足そうかと考えていたところですし。

エイリアスの場合

まずはポジティブコントロールを試してみます。v3のときの経験からこれは警告されるはずです。

namespace foo
{
using hoge = int;
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/gKWiLlgb4RUmofz6

されました。

prog.cc:7:5: warning: declaration of 'hoge' shadows a global declaration [-Wshadow]
    7 |     hoge = 0,
      |     ^~~~
prog.cc:4:7: note: shadowed declaration is here
    4 | using hoge = int;
      |       ^~~~

では逆だとどうなるでしょう。

namespace foo
{
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
using hoge = int;
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/y5dyWmnUIRnU2hEK

あれ、警告出ませんね……。

この時点で若干雲行きがあやしい気がします。main内でusing enum foo::barusing namespace foo;したら衝突しますよね?

namespace foo
{
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
using hoge = int;
} // foo

int main()
{
    using enum foo::bar;
    using namespace foo;
    hoge x = 42;
    return 0;
}

https://wandbox.org/permlink/CuTOonWZ8V3crmYE

いや、実際にhogeを使ったときだけエラーになるようですね。上のコードからhoge x = 42;を消すと警告も出ません。

prog.cc: In function 'int main()':
prog.cc:16:9: error: expected ';' before 'x'
   16 |     hoge x = 42;
      |         ^~
      |         ;
prog.cc:16:5: warning: statement has no effect [-Wunused-value]
   16 |     hoge x = 42;
      |     ^~~~

結構変な挙動の雰囲気が漂ってきました。

構造体の場合

構造体だとどうなるんでしょうか。

namespace foo
{
struct hoge {int x;};
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/0Ts21RvftAp7KkWR

警告出ませんね?

逆にしたらどうでしょう。

namespace foo
{
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
struct hoge {int x;};
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/MOaAohd0nZTJhPDX

出ませんね。まあ前ので出なかったならこれも出ないでしょう。

しかしどうしてなんでしょうか。 名前衝突の観点からは、型エイリアスとそんなに違うことはしていないと思いますが。あっでももしかしてtypedef struct {int x;} hoge;にしたら警告出ますこれ?

namespace foo
{
typedef struct {int x;} hoge;
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/soh5yCuIdkYqZRUl

あっ出た。まあこれは完全にusingと同じと言えるので、そうですね。出ますね。

ご想像の通り逆にすると、出ません。

https://wandbox.org/permlink/esZ1NioPCDZOFflQ

inline変数の場合

型ではなく値の場合はどうでしょう。

namespace foo
{
inline int hoge = 42;
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/gll9vRlP0JzCbruY

これは警告が出ますね。まあ、型エイリアスで警告が出るならこれも出ないと変です。

prog.cc:6:5: warning: declaration of 'hoge' shadows a global declaration [-Wshadow]
    6 |     hoge = 0,
      |     ^~~~
prog.cc:3:12: note: shadowed declaration is here
    3 | inline int hoge = 42;
      |            ^~~~

逆だと?

namespace foo
{
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
inline int hoge = 42;
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/JLODEtxHGP7W14Hf

出なくなりました。ある意味で一貫性はあるものの、この挙動はちょっと、なんというか……。

関数の場合

あとよく使うのは関数ですかね。

namespace foo
{
inline int hoge() {return 42;}
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/pLa5WtmhdAtICBVY

あれ、出ないんですね、警告。

一応逆も見ますか。

namespace foo
{
enum class bar
{
    hoge = 0,
    piyo = 1,
    fuga = 2
};
inline int hoge() {return 42;}
} // foo

int main()
{
    return 0;
}

https://wandbox.org/permlink/f16EnX7iaTIdYsS0

出ない。まあはい。

まとめ

うーん、パターンが掴めそうな感じはありますが、どうにもいくつかの例はシンプルに意図しない挙動のようにも見えてきますね……。

そもそも定義する順番を変えると警告が出たり出なかったりするのがまず変です。下流でのリスクは同じですよね? いや今回はリスクがほぼないので(using enumが使われない限り)警告しない方が正しそうなんですが、片方で警告してもう片方で警告しない理由が思いつきません。

より安全な状況で警告を出しておきながら、絶対に衝突させるという硬い意思がないと書かないだろうusing enumusing namespaceを足してもそれだけではshadowingの警告が出ず、使ってはじめてエラーになるというのもどうも一貫性に欠けるように感じます。 この挙動全体を俯瞰すると、意図して設計されたように見えません。

shadowingを起こし得るexprのリストみたいなのがコンパイラにはあって、それに当てはまる場合だけチェックしている(から後で出てこないとチェックが走らない)とかですかね。shadowingという対象を考えるとそうするのが都合良さそうですし。でもそれだとinline変数を後ろにやったときもチェックは走るべきか……?

うーん、背後のルールはよくわからないですが、ある程度状況がわかったのでこの辺にしておきます。多分このくらいの情報があれば回避可能でしょう。本気で背後のルールを探ろうとするとgccの実装見ないといけませんし。

今日はもう遅いので寝ます。


一応注意しておくんですが、enum classがあるとちょっと妙な挙動をするというだけで、-Wshadowは本来の用途では非常に便利な警告です。

int main()
{
    int x = 42;
    for(int i=0; i<10; ++i)
    {
        double x = 3.14;
    }
    return 0;
}

こういう場合、

https://wandbox.org/permlink/OLIJ8QBRjHt2axrE

prog.cc: In function 'int main()':
prog.cc:6:16: warning: declaration of 'x' shadows a previous local [-Wshadow]
    6 |         double x = 3.14;
      |                ^
prog.cc:3:9: note: shadowed declaration is here
    3 |     int x = 42;
      |         ^
prog.cc:6:16: warning: unused variable 'x' [-Wunused-variable]
    6 |         double x = 3.14;
      |                ^
prog.cc:3:9: warning: unused variable 'x' [-Wunused-variable]
    3 |     int x = 42;
      |         ^

のような警告で教えてくれます。このshadowingが意図しないものだった場合、この警告はかなり貴重です。