【C++】explicitキーワードとは?|意図しない暗黙の型変換を防ぐ

explicit keyword thumbnail C++

「explicitって何のために付けるの?」「コンストラクタが1個の引数を取るときによく見るけど、無いと何が困るの?」「型変換が勝手に起きるってどういうこと?」

こんな疑問はありませんか?

今回は、C++のexplicitキーワードについて、実際にコンパイル・実行した結果を交えながら解説していきます。explicitが無いとどんな「静かなバグ」が起きるのかを、実際に再現して確認します。

explicitとは?

explicitとは、

・引数を1つだけ取るコンストラクタに付けて、その型への「暗黙の型変換」を禁止するキーワード

です。C++では、引数1つのコンストラクタが定義されていると、その引数の型からクラス型への変換がコンパイラによって自動的に行われます。これは便利な場面もありますが、意図しない事故につながることも多い機能です。

【実験】explicitが無いと何が起きるか

プレイヤーの最大HPを表すHealthBarクラスと、それを受け取るSetPlayerHealth関数を用意します。

class HealthBar {
private:
    int maxHp;
public:
    HealthBar(int maxHp_) : maxHp(maxHp_) {} // explicitを付けていない
    int GetMaxHp() const { return maxHp; }
};

void SetPlayerHealth(HealthBar bar) {
    printf("プレイヤーのHPを %d に設定しました\n", bar.GetMaxHp());
}

int main() {
    int level = 5; // プレイヤーのレベル(HPとは全くの別物)

    // 本当はSetPlayerHealth(HealthBar)を呼ぶつもりはなかったのに、
    // int -> HealthBar への暗黙変換が起きて、コンパイルが通ってしまう
    SetPlayerHealth(level);

    return 0;
}

level(プレイヤーのレベル)を、HealthBarを受け取る関数にそのまま渡しています。型が違うので普通ならエラーになりそうですが、実際にコンパイル・実行してみます。

プレイヤーのHPを 5 に設定しました

エラーはおろか警告すら出ず、コンパイルも実行も普通に成功してしまいましたlevel(値は5)が、コンパイラによって暗黙にHealthBar(5)へ変換され、あたかも「HPを5に設定するつもりだった」かのように動いてしまっています。実際のゲームでは、こういう「型は合っているように見えて意味が違う値」が紛れ込む事故が起こりえます。

explicitを付けて直してみる

コンストラクタの先頭にexplicitを1つ足すだけです。

class HealthBar {
private:
    int maxHp;
public:
    explicit HealthBar(int maxHp_) : maxHp(maxHp_) {} // explicitを付けた
    int GetMaxHp() const { return maxHp; }
};

同じSetPlayerHealth(level);という呼び出しをそのままコンパイルすると、今度はこうなりました。

explicit_fixed_fail.cpp(17): error C2664: 'void SetPlayerHealth(HealthBar)': 引数 1 を 'int' から 'HealthBar' へ変換できません。
explicit_fixed_fail.cpp(17): note: class 'HealthBar' のコンストラクターが 'explicit' と宣言されています。
explicit_fixed_fail.cpp(11): note: 'SetPlayerHealth' の宣言を確認してください
explicit_fixed_fail.cpp(17): note: 引数リスト '(int)' を一致させようとしているとき

「コンストラクターがexplicitと宣言されています」と、原因まではっきり教えてくれるコンパイルエラーになりました。呼び出す側はSetPlayerHealth(HealthBar(level))のように明示的に変換を書かないと通らなくなります。この1行の差が、さっきの「静かなバグ」を未然に防いでくれるわけです。

explicitのよくある失敗例と対処法

①引数1つのコンストラクタに何となくexplicitを付け忘れる

上で実演した通りです。引数を1つだけ取るコンストラクタは、基本的にexplicitを付けるのを標準にしておくのがおすすめです。「暗黙変換させたい」という明確な意図があるときだけ外します。

②本当は暗黙変換させたいのにexplicitを付けてしまう

例えばstd::stringのように「const char*から自然に変換できてほしい」クラスにexplicitを付けると、逆に不便になります。「間違った値が紛れ込みやすい型かどうか」で判断しましょう。

③explicitを付けてもコピー初期化以外は防げないと誤解する

explicitは「=による暗黙変換」を防ぎますが、HealthBar bar(level);HealthBar bar{level};のように直接コンストラクタを呼ぶ書き方は防げません。explicitが防ぐのはあくまで「意図せず変換されるパターン」であり、明示的な生成そのものは今まで通り可能です。

注意点

  • 引数1つのコンストラクタは基本explicitを付ける
  • explicit無しだとエラーどころか警告すら出ないことを実際に確認した
  • 自然な変換をさせたい型(string等)には付けない
  • 直接コンストラクタを呼ぶ書き方はexplicitがあっても引き続き可能

まとめ

  • explicitは引数1つのコンストラクタによる暗黙の型変換を禁止する
  • 無いと型が違う値がエラー無しですり抜けてしまう(実際に確認した)
  • 付けると、コンパイラが原因まで教えてくれるエラーで気づける
  • 引数1つのコンストラクタは、基本explicitを付けておくのが安全

explicitは地味なキーワードですが、「エラーも警告も出ないまま意味の違う値が渡ってしまう」という、実際に見ると背筋が伸びるバグを防いでくれます。引数1つのコンストラクタを書くたびに、付けるかどうか一度立ち止まる価値があります。

ここまで読んでくださりありがとうございました。

関連記事