閉じる

C++のexplicitの意味とは?暗黙の型変換を禁止する使い方を解説

C++におけるプログラミングでは、意図しない挙動を防ぎ、プログラムの堅牢性を高めることが非常に重要です。

そのために欠かせないキーワードの一つがexplicitです。

このキーワードは、主にクラスのコンストラクタや変換関数に付与され、コンパイラによる「暗黙の型変換」を制限する役割を果たします。

本記事では、explicitの基本的な意味から、C++20で導入された最新の仕様まで、具体的なコード例を交えて詳しく解説します。

explicitキーワードの基本概念

C++では、特定の条件を満たすと、プログラマが明示的に指示しなくてもコンパイラが自動的に型を変換してくれる機能があります。

これを「暗黙の型変換」と呼びます。

非常に便利な機能である反面、予期せぬバグの原因となることが多いため、明示的な呼び出しのみを許可するためにexplicitを使用します。

暗黙の型変換が引き起こす問題

まずは、explicitを付けない場合にどのような挙動になるかを確認しましょう。

C++のクラスにおいて、引数を一つだけ取るコンストラクタは、その引数の型からクラスの型への変換ルールとして定義されます。

C++
#include <iostream>

class MySize {
public:
    // explicitを付けていないコンストラクタ
    MySize(int size) : value(size) {
        std::cout << "Constructor called with: " << value << std::endl;
    }

    int getValue() const { return value; }

private:
    int value;
};

void printSize(const MySize& s) {
    std::cout << "Size is: " << s.getValue() << std::endl;
}

int main() {
    // 通常のオブジェクト生成
    MySize s1(10);

    // 暗黙の型変換による生成(代入のような形式)
    MySize s2 = 20; 

    // 関数呼び出し時の暗黙の変換
    // MySizeオブジェクトを渡すべき場所にintを渡せてしまう
    printSize(30); 

    return 0;
}
実行結果
Constructor called with: 10
Constructor called with: 20
Constructor called with: 30
Size is: 30

上記のコードでは、printSize(30)という呼び出しが成功しています。

本来、printSize関数はMySize型のオブジェクトを引数に取りますが、コンパイラが「intからMySizeへの変換方法(コンストラクタ)」を知っているため、自動的に一時的なオブジェクトを作成して関数に渡してしまいます。

これは便利なようですが、大きなプログラムでは「ただ数値を渡しただけのつもりが、勝手にオブジェクトが作られていた」という状況を生み、可読性や安全性を損なう原因となります。

explicitによる暗黙の変換の禁止

ここで、コンストラクタにexplicitを付与してみましょう。

これにより、先ほどのような「=」を用いた初期化や、関数引数での自動変換が禁止されます。

explicitコンストラクタのコード例

C++
#include <iostream>

class MySize {
public:
    // explicitを付与
    explicit MySize(int size) : value(size) {}

    int getValue() const { return value; }

private:
    int value;
};

void printSize(const MySize& s) {
    std::cout << "Size is: " << s.getValue() << std::endl;
}

int main() {
    // OK: 直接初期化
    MySize s1(10);

    // OK: 中括弧を用いた直接初期化
    MySize s2{20};

    // NG: コンパイルエラー(暗黙の変換は不可)
    // MySize s3 = 30;

    // NG: コンパイルエラー(intをMySizeに勝手に変換できない)
    // printSize(40);

    // OK: 明示的にコンストラクタを呼ぶかキャストする
    printSize(MySize(50));
    printSize(static_cast<MySize>(60));

    return 0;
}

explicitを付けることで、MySize s3 = 30;のような記述はコンパイルエラーとなります。

プログラマはMySize(50)のように「今からMySizeオブジェクトを作る」という意思を明確に示す必要が出てきます。

これにより、コードの意図がはっきりとし、予期せぬ型変換によるバグを未然に防ぐことができます。

変換関数におけるexplicit

explicitキーワードは、コンストラクタだけでなくユーザー定義の変換関数(operator T())にも使用できます。

変換関数の落とし穴

クラスを特定の型(例えばbool型やint型)として扱いたい場合、変換関数を定義します。

しかし、これにも暗黙の変換のリスクが伴います。

C++
class MySafeInt {
public:
    MySafeInt(int v) : value(v) {}

    // bool型への変換関数(explicitなし)
    operator bool() const {
        return value != 0;
    }

private:
    int value;
};

int main() {
    MySafeInt a(10);
    MySafeInt b(20);

    // 本来は if (a) のように使いたい
    if (a) { /* ... */ }

    // しかし、explicitがないと数値として計算できてしまう
    // aとbがboolに変換され、そのboolがint(1 or 0)として加算される
    int result = a + b; 
    
    return 0;
}

上記の例では、a + bという式が成立してしまいます。

これは、MySafeIntboolに変換され、そのboolがさらにintに昇格して計算されるためです。

オブジェクト同士の足し算を意図していない場合、これはバグになります。

explicit変換関数の活用

変換関数にexplicitを付けると、条件式(ifやwhileの判定)などの特定の文脈を除き、暗黙の変換が行われなくなります。

C++
class MySafeInt {
public:
    MySafeInt(int v) : value(v) {}

    // explicitを付与した変換関数
    explicit operator bool() const {
        return value != 0;
    }

private:
    int value;
};

int main() {
    MySafeInt a(10);

    // OK: if文の中などは「文脈上の変換」として許容される
    if (a) { 
        // 実行される
    }

    // NG: コンパイルエラー(暗黙的にintに変換して計算することはできない)
    // int x = a + 5;

    // OK: 明示的なキャストなら可能
    bool b = static_cast<bool>(a);

    return 0;
}

このように、変換関数にexplicitを付けることで、「条件判定には使いたいが、勝手に数値として扱われたくない」という設計が可能になります。

標準ライブラリのstd::shared_ptrstd::unique_ptrなどのスマートポインタも、この仕組みを利用して「ポインタが有効かどうかの判定」のみを安全に許可しています。

C++20以降の進化:explicit(bool)

C++20からは、さらに高度な制御が可能になりました。

それが条件付きexplicitです。

これは、テンプレート引数などの条件に応じて、そのコンストラクタをexplicitにするかどうかを動的に切り替える機能です。

なぜ条件付きexplicitが必要なのか

例えば、std::pairのようなコンテナクラスを考えてみましょう。

中に入れる型が暗黙の変換を許可しているならpairも許可したいし、中身がexplicitならpairexplicitにしたい、という要求があります。

これを実現するのが explicit(bool-expression) 構文です。

C++
#include <iostream>
#include <type_traits>

template <typename T>
struct Wrapper {
    T value;

    // Tがint型の場合のみexplicitにする、という条件付き指定
    explicit(std::is_same_v<T, int>)
    Wrapper(T v) : value(v) {}
};

int main() {
    // T = int なので、このコンストラクタは explicit になる
    // Wrapper<int> w1 = 10; // エラー
    Wrapper<int> w1(10);    // OK

    // T = double なので、このコンストラクタは explicit にならない
    Wrapper<double> w2 = 3.14; // OK
    
    std::cout << "w1: " << w1.value << ", w2: " << w2.value << std::endl;

    return 0;
}
実行結果
w1: 10, w2: 3.14

この機能により、ライブラリ開発者はユーザーに提供するクラスの挙動を、型安全性を保ちつつ柔軟に制御できるようになりました。

explicitをいつ使うべきか

実務において、どのような基準でexplicitを付けるべきか、ガイドラインをまとめました。

カテゴリ推奨される対応理由
1引数コンストラクタ原則として付ける意図しない暗黙の変換はバグの温床となるため。
多引数コンストラクタ必要に応じて付けるC++11以降、{}による初期化で暗黙の変換が起こる可能性があるため。
変換関数 (operator T)原則として付けるif文での利用に限定し、予期せぬ演算への混入を防ぐため。
コピー/ムーブコンストラクタ付けないこれらに付けると、値渡しなどができなくなり、言語の基本動作が阻害される。

リスト初期化と多引数コンストラクタの注意点

C++11以降では、複数の引数を持つコンストラクタに対しても暗黙の変換(リスト初期化)が影響します。

C++
class Rect {
public:
    // 2引数だが explicit を付ける
    explicit Rect(int width, int height) {}
};

void draw(Rect r) {}

int main() {
    // explicit がないと、これを通せてしまう(Rectオブジェクトに見えない)
    // draw({10, 20}); 

    // explicit があると、上記はエラーになり、以下のように書く必要がある
    draw(Rect{10, 20}); 
}

このように、引数が複数であっても「波括弧による初期化」が意図しない文脈で行われるのを防ぐためにexplicitは有効です。

まとめ

explicitキーワードは、C++プログラミングにおける「安全装置」のような存在です。

コンパイラによる過剰な親切心(暗黙の型変換)を抑制し、コードの挙動をプログラマの制御下に置くことができます。

1引数のコンストラクタを定義する際は、まず「explicitを付ける」ことをデフォルトの選択肢とし、どうしても暗黙の変換が必要な場合に限って外すというスタンスが、モダンなC++開発では推奨されます。

C++20で導入されたexplicit(bool)も含め、このキーワードを適切に使いこなすことで、可読性が高く、メンテナンスのしやすい堅牢なプログラムを構築することができるでしょう。

クラスの定義と基本

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!