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;
}
全くなぜだかコンパイルが通る
そして実行時にセグフォる
意味がわからない
一応、val.x
がconst
になっていることを確認してみる
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.x
はconst int&
だってさっき確認したし、
operator>>(std::istream&, const int&)
なんてオーバーロードは存在しないはずd...あ゛あ゛あ゛あ゛あ゛
Hoge(int y) : x(y) {}
こいつだああぁぁぁ
つまり
-
operator>>(std::istream&, const int&)
というオーバーロードは存在しない -
const int&
から暗黙に変換可能な型からオーバーロード先が検索される -
const int&
はHoge
に暗黙に変換できる(is >> Hoge(val.x);
みたいにできる) - このままでは
>>
の右オペランドはHoge
だが、Hoge
はconst Hoge&
で束縛できる(const参照は右辺値を束縛できる) - よって
is >> val.x;
でoperator>>(std::istream&, const Hoge&)
が呼び出されることで無限に再帰が発生する(今回は関係ないけど多分UB)
というわけで、コンストラクタにexplicit
2をつけることで無事にコンパイルエラーにできました
wandbox
何、コンパイルエラーの量が多いだって?だって当然だろ?C++なら!
そんなわけで
引数が1つのコンストラクタには可能な限りexplicit
を付けましょう
一応、explicit
がないバージョンだと
mint x = 0;
x += mint(2); // こう書くのが本来だが
x += 2; // こうも書ける
みたいな記述が可能になるので付けないことにも利点があるように見えるが、変な間違いを防ぐためにはexplicit
を付けておいたほうがいいだろう
explicitを付けた場合は上のコードみたいに+= mint(2)
と毎回コンストラクタを明示的に呼ぶか、演算用のオーバーロードに整数型との演算を追加しておけば良い(ただし結構な数になる3)
今回はコピペして使うライブラリということであまりに長くなるのは嬉しくないだろうから、私は前者をおすすめする
-
以前から公開されているライブラリなので ↩
-
一応説明しておくと、これを付けたコンストラクタや変換オーバーロード(
operator int()
とか)は今回みたいな暗黙の変換がなされることがなくなります。詳しくはhttps://ja.cppreference.com/w/cpp/language/explicit ↩ -
各2項演算ごとに(mint,mint), (mint, int), (int, mint)で3つ、各複合代入演算ごとに(mint, mint), (mint, int) で2つ必要になる。boostを使えばある程度省略できるとは思うが、初心者向けの解説だしメタプログラミングはあまり喜ばれないだろう ↩