閉じる

【C++】const_castの正しい使い方とNG例

C++で既存のコードを保守したり、レガシーなAPIと連携したりしていると、どうしてもconstを外したくなる場面に出会います。

そのためのキャストがconst_castです。

しかし、この機能を誤って使うと、動作未定義やバグの温床になってしまいます。

本記事では、C++のconst_castの正しい使い方とやってはいけないNG例を、図解とサンプルコードを用いて丁寧に解説します。

const_castとは何か

const_castの役割と特徴

C++におけるconst_castは、型の「const」「volatile」修飾を付け外しするためのキャストです。

特に多く使われるのはconstを外す用途です。

ポイントを文章で整理すると、次のような性質を持ちます。

  • ポインタや参照の「const性」を変更できる
  • オブジェクトそのものの性質(本当にconstかどうか)は変えられない
  • 本当にconstなオブジェクトを書き換えると動作未定義

つまり「見かけ上constに見せているだけのオブジェクト」からconstを外すときだけ、安全に使えるということです。

const_castの基本構文

C++のキャストはテンプレート呼び出しのような構文で行います。

C++
#include <iostream>

int main() {
    const int value = 10;        // constなint
    const int* pConst = &value;  // const intへのポインタ

    // const_castによるキャスト構文
    int* p = const_cast<int*>(pConst);  // constを外す

    std::cout << "アドレス: " << p << std::endl;
    return 0;
}

この構文の読み方は「pConstint*にconst_castする」です。

const_castでできること・できないこと

「ポインタ/参照のconst」と「オブジェクトのconst」の違い

C++では、constがどこに掛かっているかで意味が変わります。

  • const int* p
    「intがconst」つまり指す先の値が変更不可
  • int* const p
    「ポインタpがconst」つまりポインタのアドレス変更が不可
  • const int* const p
    両方がconst

const_castは、これらの「const修飾」を外すことができますが、オブジェクト自体がコンパイル時からconstとして確保されているかどうかまでは変えられません。

const_castで外せるのは「見かけ上」のconstだけ

例として、次の2つの状況を比べます。

C++
void safe_case() {
    int x = 10;             // 元は非const
    const int* p = &x;      // constなポインタとして受け取る

    int* q = const_cast<int*>(p); // constを外す(元は非constなのでOK)
    *q = 20;                       // 安全に書き換え可能
}

void unsafe_case() {
    const int y = 10;       // 最初からconst
    const int* p = &y;

    int* q = const_cast<int*>(p); // constは外せるが…
    *q = 20;                       // 動作未定義(やってはいけない)
}

見かけ上だけconstにしたものを戻すのはOKですが、最初からconstとして定義されたオブジェクトを変更するのはNGです。

const_castの典型的な正しい使い方

1. 古いAPIとの互換性のためにconstを外す

古いCスタイルのAPIでは、引数としてcharを要求しているが、実際には文字列を書き換えない関数が存在します。

そのような関数を、C++側からconst charの文字列で呼びたい場合にconst_castが使われます。

C++
#include <iostream>

// 古いCスタイルのAPI(実際には文字列を書き換えない)
void legacy_print(char* s) {
    std::cout << s << std::endl;
}

int main() {
    const char* msg = "Hello, const_cast"; // 実際には文字列リテラル

    // NG例: これは本来危険
    // legacy_print(const_cast<char*>(msg));

    // 安全にするには、書き換えて良いバッファを用意する
    char buffer[64];
    std::snprintf(buffer, sizeof(buffer), "%s", msg);

    // レガシーAPIは実際には書き換えない前提
    legacy_print(buffer);  // const_cast不要

    return 0;
}

この例では、あえてconst_castを避けています。

本当に書き換えないAPIであると保証できるなら、バッファを自前で用意してから渡す方が安全だからです。

とはいえ、「APIのシグネチャが古いだけで、中身は絶対に書き換えない」と分かっている場合、引数にconst_cast<char*>(ptr)を使うケースは現実には存在します。

2. constメンバ関数内でキャッシュを書き換える

クラス設計でよく出てくるパターンが「constメンバ関数内で、計算結果のキャッシュだけを書き換えたい」というケースです。

C++
#include <iostream>

class Data {
    int value_;
    mutable bool cached_;  // キャッシュ済みフラグ
    mutable int cache_;    // キャッシュ値

public:
    Data(int v) : value_(v), cached_(false), cache_(0) {}

    int getValue_const_cast() const {
        if (!cached_) {
            // constメンバ関数内で自分自身の非const参照を取得
            Data* self = const_cast<Data*>(this);
            self->cache_ = value_ * 2; // キャッシュを計算して保存
            self->cached_ = true;
        }
        return cache_;
    }

    int getValue_mutable() const {
        if (!cached_) {
            cache_ = value_ * 2; // mutableなのでconstでも代入可能
            cached_ = true;
        }
        return cache_;
    }
};

int main() {
    Data d(10);

    std::cout << d.getValue_const_cast() << std::endl;
    std::cout << d.getValue_mutable() << std::endl;

    return 0;
}
実行結果
20
20

このケースでは、設計としてはmutableを使う方がC++的で自然です。

ただ、既存コードの制約などでmutableが使えない場合に限り、const_castでselfを書き換えるというテクニックが使われます。

const_castのNG例・危険な使い方

1. 本当にconstなオブジェクトを書き換える

最も典型的なNG例です。

C++
#include <iostream>

int main() {
    const int x = 10;
    const int* p = &x;

    int* q = const_cast<int*>(p);
    *q = 20;  // 動作未定義。コンパイルは通るが、何が起きるか分からない

    std::cout << x << std::endl;
    return 0;
}

このコードはコンパイルが通ってしまいますが、実行結果は処理系に依存し、クラッシュすることすらあります

「コンパイルが通るから安全」と思わないことが重要です。

2. 文字列リテラルを書き換える

文字列リテラルは、C++では実質的に書き換え不可な領域に置かれます。

それをconst_castなどで書き換えようとすると危険です。

C++
#include <iostream>

int main() {
    const char* s = "Hello";

    char* p = const_cast<char*>(s);
    p[0] = 'X';  // 多くの環境でクラッシュ、または未定義動作

    std::cout << s << std::endl;
    return 0;
}

このようなコードは絶対に書いてはいけないレベルのNG例です。

書き換えたいのであれば、必ず書き換え可能なバッファにコピーしてから編集します。

C++
#include <iostream>
#include <cstring>

int main() {
    const char* s = "Hello";

    char buffer[16];
    std::strncpy(buffer, s, sizeof(buffer));
    buffer[0] = 'X';  // 安全に書き換え可能

    std::cout << buffer << std::endl; // Xello
    return 0;
}
実行結果
Xello

3. 「const設計」を無理にねじ曲げる

constは「このメンバ関数はオブジェクトを論理的に変更しません」という契約を表す機能です。

それにもかかわらず、constメンバ関数の中で何でもかんでもconst_castで書き換えてしまうと、設計としてのconstの意味が崩壊し、コード全体の信頼性が落ちます

このような場合、本当に変更したい状態なのか、あるいは「論理的には変更ではないキャッシュ」のようなものなのかを見直し、必要ならmutableを用いるべきです。

const_castと他のキャストとの違い

Cスタイルキャストとの比較

Cスタイルキャスト(T)exprは、複数のキャストを一度に行おうとする危険なキャストです。

一方でC++のconst_castは、「const/volatile修飾の付け外し専用」で、役割が明確です。

Cスタイルキャストでconstを外すことは可能ですが、何が行われているか分かりにくくなるため避けるべきです。

C++
void func(const int* p) {
    // 悪い例: Cスタイルキャストでconstを外す
    int* q1 = (int*)p;

    // 良い例: 明示的にconst_castを使う
    int* q2 = const_cast<int*>(p);

    (void)q1;
    (void)q2;
}

レビュー時に(int*)が何をしているのかを一々読み解くのは負担が大きく、バグを見逃しやすくなります。

static_cast / reinterpret_cast との役割分担

キャストの種類と用途を簡単な表にまとめます。

キャスト主な用途
const_castconst/volatile修飾の付け外し
static_cast安全な型変換(数値変換、上位/下位クラスなど)
reinterpret_castポインタのビットレベル再解釈など危険な変換
dynamic_cast継承階層間の安全なダウンキャスト
Cスタイルキャスト上記をまとめてやろうとする(避けるべき)

「何をしたいのか」が読み手に伝わるキャストを選ぶのが、読みやすく安全なC++コードへの第一歩です。

const_castを使うときの指針

使ってよい場面の判断基準

const_castを使って良い場面かどうかは、次のような基準で判断すると分かりやすくなります。

  1. 元のオブジェクトは、どこかで非constとして作られているか
  2. そのオブジェクトは、本来書き換えても問題ない性質のものか
  3. constになっているのは「インターフェース上の都合」だけか
  4. 代わりにmutableやAPI設計の見直しで解決できないか

これらを満たす場合に限り、局所的かつ最小限にconst_castを使うのがおすすめです。

どうしても必要なときの注意点

どうしてもconst_castが必要なケースでは、次の点に注意するとリスクを減らせます。

  • キャストの直前に、なぜ安全かをコメントで明示する
  • キャストしたポインタ/参照のスコープをできるだけ短くする
  • 書き換え対象が本当に非const由来かをコード上で追いやすくする
C++
void process_buffer(const int* p, std::size_t n) {
    // 元は非constバッファからしか呼ばれない前提
    // 書き換えは許容されるのでconst_castを使用
    int* writable = const_cast<int*>(p);

    for (std::size_t i = 0; i < n; ++i) {
        writable[i] *= 2;  // バッファを更新
    }
}

このように、「なぜこのconst_castは安全なのか」をコメントで説明しておくと、保守する人も安心できます。

まとめ

const_castは、C++でconst/volatile修飾を付け外しするためだけに用意された、用途限定のキャストです。

元が非constなオブジェクトをインターフェース上の都合でconstとして扱い、必要な場面で元に戻すといった、限定的な状況でのみ安全に使えます。

一方で、本当にconstなオブジェクトや文字列リテラルを書き換えると動作未定義になり、大きなバグにつながります

const_castを見かけたら、「なぜ必要なのか」「元のオブジェクトは非constか」を必ず確認し、可能ならmutableやAPI設計の見直しで代替することを意識すると、安全で読みやすいC++コードを書くことができます。

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

URLをコピーしました!