LoginSignup
4
3

More than 3 years have passed since last update.

コンストラクタにexplicitを付けていない時に起きる混乱の例の一つ

Last updated at Posted at 2020-06-27

TL;DR

(copy/move ctor以外の)引数が一つのコンストラクタには(特別な理由がなければ(@yumetodo氏のコメント参照))explicitをつけろ

... いつもどおりだな!(C++erが口を揃えて常に言い続けていることだし)

発端

先程、AtCoderABC172の解説放送(2:51:00へのリンク)を見ていたときのこと

競技プログラミングの事はこの記事の本筋ではないので省くが、画面にはこんなコードが写っていた(説明のため重要でない部分を変更/省略してます)

struct mint {
    int x;
    mint(int x=0) : x(x) {}
    mint operator-() const { ...
    ...
};
std::istream& operator>>(std::istream& is, const mint& a) {
    return is >> a.x;
}
std::ostream& operator<<(std::ostream& os, const mint& a) {
    return os << a.x;
}

自分はこのコードをよく見ていなかった1ので気づかなかったが、youtube liveのコメントで 「>>のオーバーロードにconstがついてる」 という指摘がされた

確かによく見ると右オペランドに非const参照を取るべきストリーム入力のオーバーロードがconst&で定義されている

...

なんでこれ、コンパイル通ってるの?

operator>>(std::istream&, const int&)なんてオーバーロードは存在しないはずなので、コンパイルエラーになるはずだ 何かがおかしい

(このオーバーロード全然使ってないみたいで、書いた人(snukeさん)も問題に気付いていなかったようです)

調べてみる

重要な部分以外を省いた+検証用のmainを追加したコードがこちら

#include <iostream>

struct Hoge {
    int x;
    Hoge(int y) : x(y) {}
};
std::istream& operator>>(std::istream& is, const Hoge& val) {
    return is >> val.x;
}

int main() {
    Hoge x{3};
    std::cin >> x;
}

wandbox

全くなぜだかコンパイルが通る
そして実行時にセグフォる

意味がわからない

一応、val.xconstになっていることを確認してみる

std::istream& operator>>(std::istream& is, const Hoge& val) {
    static_assert(std::is_same_v<decltype((val.x)), const int&>);
    return is >> val.x;
}

wandbox
(decltypeが二重カッコの理由はこちら)

コンパイルが通る

意味がわからない

コンパイラを変えてみた

ここまでgccを使っていたので、エラーメッセージが解りやすいことの多いclangでコンパイルしてみる
wandbox

コンパイルが..ん?よく見ると警告が出ている(結局コンパイルは通ってる)

prog.cc:8:60: warning: all paths through this function will call itself [-Winfinite-recursion]
std::istream& operator>>(std::istream& is, const Hoge& val) {

...無限再帰????

つまりis >> val.x;operator>>(std::istream&, const Hoge&)を呼び出しているということだ

え?val.xconst int&だってさっき確認したし、
operator>>(std::istream&, const int&)なんてオーバーロードは存在しないはずd...あ゛あ゛あ゛あ゛あ゛

Hoge(int y) : x(y) {}

こいつだああぁぁぁ

つまり

  1. operator>>(std::istream&, const int&)というオーバーロードは存在しない
  2. const int&から暗黙に変換可能な型からオーバーロード先が検索される
  3. const int&Hogeに暗黙に変換できる(is >> Hoge(val.x);みたいにできる)
  4. このままでは>>の右オペランドはHogeだが、Hogeconst Hoge&で束縛できる(const参照は右辺値を束縛できる)
  5. よって is >> val.x;operator>>(std::istream&, const Hoge&)が呼び出されることで無限に再帰が発生する(今回は関係ないけど多分UB)

というわけで、コンストラクタにexplicit2をつけることで無事にコンパイルエラーにできました
wandbox

何、コンパイルエラーの量が多いだって?だって当然だろ?C++なら!

そんなわけで

引数が1つのコンストラクタには可能な限りexplicitを付けましょう

一応、explicitがないバージョンだと

mint x = 0;
x += mint(2); // こう書くのが本来だが
x += 2; // こうも書ける

みたいな記述が可能になるので付けないことにも利点があるように見えるが、変な間違いを防ぐためにはexplicitを付けておいたほうがいいだろう

explicitを付けた場合は上のコードみたいに+= mint(2)と毎回コンストラクタを明示的に呼ぶか、演算用のオーバーロードに整数型との演算を追加しておけば良い(ただし結構な数になる3
今回はコピペして使うライブラリということであまりに長くなるのは嬉しくないだろうから、私は前者をおすすめする


  1. 以前から公開されているライブラリなので 

  2. 一応説明しておくと、これを付けたコンストラクタや変換オーバーロード(operator int()とか)は今回みたいな暗黙の変換がなされることがなくなります。詳しくはhttps://ja.cppreference.com/w/cpp/language/explicit 

  3. 各2項演算ごとに(mint,mint), (mint, int), (int, mint)で3つ、各複合代入演算ごとに(mint, mint), (mint, int) で2つ必要になる。boostを使えばある程度省略できるとは思うが、初心者向けの解説だしメタプログラミングはあまり喜ばれないだろう 

4
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3