閉じる

C++のconst_castの使い方と危険性を基礎から解説

C++ではconstで「読み取り専用」の約束を付けられますが、やむを得ずそれを一時的に外したい場面があります。

そのための道具がconst_castです。

本記事では、仕組みから安全な使い方、避けるべき危険な例、代替手段までを初心者の方にもわかりやすく整理します。

結論として、constを外すのは最後の手段であり、基本はAPI設計でconst対応を整えることが最善です。

C++のconst_castとは?

const(読み取り専用)の基本

C++のconstは「その経路からは値を書き換えない」ことをコンパイラに約束する修飾子です。

変数自体を不変にする「トップレベルconst」と、ポインタや参照が指す先だけを不変にする「ボトムレベルconst(低レベルconst)」があります。

例えばconst int xは変数そのものが不変、const int* pはポインタpが指す先のintが不変を意味します。

constは「最適化の前提」にも使われるため、約束を破るとプログラムの振る舞いは保証されません。

用語のポイントとして、T* constは「ポインタ自体がconst(差し先は変更可)」、const T*は「指す先のTがconst(ポインタは差し替え可)」と読みます。

参照T&は結び替え不可で、本質的に「常にconstな参照先」を持ちますが、参照先のオブジェクトがconstとは限りません。

const_castで外せるconstと外せないconst

const_castは型からconst(とvolatile)を外す、または付けるための明示的キャストです。

例えばconst intintに変換できます。

ただし外見上のconstを外せても、もともとのオブジェクトがconstとして定義されていた場合に書き換えると未定義動作です。

  • 外してよいのは「もともと非constとして確保されたオブジェクト」への経路についたconstだけです。
  • もともとconstとして定義されたオブジェクト(例: const int x = 1;)を書き換えるのは未定義動作です。

どんな場面で使う型変換か

現実に使うのは次のような限定的な場面です。

  • 古いC APIがcharを受け取るが実際は書き換えないため、const charから一時的にconstを外して渡したい。
  • 同一実装を再利用するために、const版と非const版のメンバー関数を整合的に実装するテクニック。
  • 内部キャッシュを書き換える必要があるが外部インターフェースとしてはconstを保ちたいケース(ただし通常はmutableを使います)。

それ以外の目的で乱用すべきではありません。

多くのケースはAPI設計(オーバーロード、const対応の引数)で解決できます。

const_castの使い方(初心者向け)

ポインタのconstを外して関数に渡す例

レガシーAPIがchar*を要求するが、実際は書き換えないとわかっている場合の例です。

安全のため、自分でラップ関数を作り、内部でconst_castして渡すと意図が明確になります。

C++
#include <iostream>
#include <string>

// レガシーAPI: 本当は読み取り専用だが、宣言が char* になっている
extern "C" void legacy_print(char* s) {
    // 文字列を出力するだけ。書き換えはしないと仮定
    std::cout << s << '\n';
}

// 安全ラッパー: const対応のAPIを用意する
void print_legacy_readonly(const char* s) {
    // ここでだけ const を外して渡す。書き換えないAPIに限る。
    legacy_print(const_cast<char*>(s));
}

int main() {
    std::string msg = "Hello, const_cast";
    // std::stringは自前の領域にデータを持つ(可変)
    print_legacy_readonly(msg.c_str());

    // 文字列リテラルは const char[] として扱われる(不可変)
    // 書き換えないAPIに限り、渡すのは可。ただし書き換えられると未定義動作。
    print_legacy_readonly("literal OK (read-only)");
}
実行結果
// 実行結果例
Hello, const_cast
literal OK (read-only)

注意: レガシーAPIが実際に書き換える可能性があるなら、constを外して渡すのは厳禁です。

代わりに変更可能なバッファを用意してコピーしましょう(後述)。

参照のconstを外すときの注意(元が非constのときだけ)

参照でも同様に、元のオブジェクトが非constであることが条件です。

次は安全なケースです。

C++
#include <iostream>

void increment(int& x) { ++x; }

// const参照から非const参照へキャスト(元のオブジェクトが非constならOK)
void increment_via_const_ref(const int& cx) {
    increment(const_cast<int&>(cx)); // 元が非constなら安全
}

int main() {
    int n = 10;           // 非const
    const int& cr = n;    // const参照だが、参照先は非constオブジェクト
    increment_via_const_ref(cr);
    std::cout << n << '\n';
}
実行結果
// 実行結果例
11

一方、本当にconstとして定義されたオブジェクトから外すのは未定義動作です。

次は「コンパイルは通るがやってはいけない」例です。

C++
// 実行しないでください: 未定義動作の例
void bad(const int& cx) {
    int& x = const_cast<int&>(cx);
    x = 42; // cxの参照先が const で確保された場合は未定義動作
}

int main() {
    const int c = 0; // 本当にconst
    bad(c);          // 未定義動作
}

constメンバー関数での扱い(オーバーロードの整理)

同じ名前でconst版と非const版を用意するのがC++の慣習です。

このとき、実装を一箇所に寄せるテクニックとして、const版を基本実装にし、非const版は戻り値にだけconst_castを使う方法があります。

C++
#include <vector>
#include <cstddef>

class Buffer {
    std::vector<int> data_;
public:
    // const版: 基本実装
    const int& at(std::size_t i) const {
        return data_.at(i);
    }
    // 非const版: const版を再利用し、戻り値のconstだけ外す
    int& at(std::size_t i) {
        return const_cast<int&>(static_cast<const Buffer&>(*this).at(i));
    }

    void push(int v) { data_.push_back(v); }
};

#include <iostream>
int main() {
    Buffer b;
    b.push(1); b.push(2);

    const Buffer& cb = b;
    std::cout << cb.at(0) << '\n'; // 読み取り
    b.at(1) = 42;                  // 書き込み(非const版)
    std::cout << cb.at(1) << '\n'; // 更新を確認
}
実行結果
// 実行結果例
1
42

ポイント: このパターンは「非constオブジェクトに対してのみ非const版が呼ばれる」というC++のオーバーロード規則に依存しており、const_castによって「本来constなオブジェクトを変更可能にする」ことはありません。

const_castの危険性と未定義動作

constオブジェクトを書き換えると未定義動作

もともとconstとして定義されたオブジェクトを書き換える行為は未定義動作です。

コンパイラは「constは変わらない」と仮定して最適化します。

よって観測結果が環境や最適化レベルで変わる、落ちるなどの問題を引き起こします。

C++
// 未定義動作の例(動かさないこと)
#include <iostream>
int main() {
    const int x = 1;
    int* p = const_cast<int*>(&x);
    *p = 999; // 未定義動作: 実行してはいけない
    std::cout << x << " " << *p << '\n';
}

文字列リテラル/グローバルconstは書き換え厳禁

文字列リテラル(例: "hello")は多くの処理系で読み取り専用領域に置かれます。

constを外して書き換えるとクラッシュや不可解な動作の原因です。

一方、char buf[] = "hello";のように配列にコピーすれば変更可能です。

C++
// 悪い例: リテラルの書き換えは未定義動作
const char* s = "hello";
// char* w = const_cast<char*>(s);
// w[0] = 'H'; // やってはいけない

// 良い例: 配列にコピーしてから変更
char buf[] = "hello";
buf[0] = 'H'; // OK

また、constグローバル変数も同様に読み取り専用領域に配置されることがあり、書き換えは未定義動作です。

constを外して渡すだけでも危険なケース

自分が書き換えなくても、受け取った関数が書き換えるかどうかは型シグネチャが語ります

シグネチャがcharなら「書き換えるかもしれない」と読まれるので、const charからのconst_castで渡すのは危険です。

さらに、メモリマップの読み取り専用領域や、共有データへの別スレッドからの書き込みなど、想定外の副作用でクラッシュする可能性もあります。

ビルドは通るが動作は保証されない

constはコンパイラ最適化の前提です。

再順序化やレジスタ保持により、const_castで書き換えても別の場所からは変わって見えない、ということが起こり得ます。

「コンパイルが通る」ことは「正しく動作する」ことの証明にはなりません。

以下に、安全/危険の目安を簡単にまとめます。

状況constを外して読み取りconstを外して書き込み
非constとして確保されたオブジェクトおおむね安全安全(設計として適切なら)
本当にconstで定義されたオブジェクト未定義動作の可能性未定義動作
文字列リテラル/グローバルconst未定義動作の可能性未定義動作
レガシーAPIが実際に書かないと保証できる条件付きで可非推奨(将来の変更で破綻)

代替手段とベストプラクティス

const対応のAPIを使う/用意する

最善は「const対応の関数を作る/選ぶ」ことです。

オーバーロードを用意して、読み取り専用データはconstのまま受け取りましょう。

C++
#include <string>
#include <iostream>

void process(const std::string& s) { // 読み取り専用
    std::cout << "view: " << s << '\n';
}
void process(std::string& s) {       // 変更する可能性あり
    s += "!";
}

int main() {
    std::string a = "abc";
    const std::string b = "xyz";
    process(a); // 非const版が呼ばれ、書き換え可
    process(b); // const版が呼ばれ、読み取りのみ
    std::cout << a << '\n';
}
実行結果
// 実行結果例
view: xyz
abc!

コピーして安全に変更する

読み取り専用のものを変更したいならコピーが基本です。

最小限のコストで安全性を買えます。

C++
#include <iostream>
#include <string>

void legacy_mutate(char* p); // 実装は省略: 書き換えるレガシーAPI

void safe_call_legacy(const char* s) {
    std::string buf = s;           // コピーして可変バッファを用意
    legacy_mutate(buf.data());     // C++17以降: data() は書き換え可
    std::cout << buf << '\n';
}

キャッシュはmutableで管理する

constメンバー関数での遅延計算/キャッシュmutableを使うのがモダンC++の定石です。

const_castthisを書き換えるのは避けましょう。

C++
#include <string>
#include <optional>

class Hasher {
    std::string data_;
    mutable std::optional<std::size_t> cache_; // const関数から更新してよい
public:
    explicit Hasher(std::string s) : data_(std::move(s)) {}

    std::size_t hash() const {
        if (!cache_) {
            std::size_t h = 1469598103934665603ull;
            for (unsigned char c : data_) { h = (h ^ c) * 1099511628211ull; }
            cache_ = h; // const関数内だがmutableなので更新可
        }
        return *cache_;
    }
};

Cスタイルキャストではなく明示的なconst_castを使う

Cスタイルキャスト(T)exprT(expr)は、複数の種類のキャストが試され、どれが行われたかが見えづらい問題があります。

constを外すならconst_castを明示し、意図と危険性をコードに刻みましょう。

C++
// 悪い例: 何が起きているかわからない
// char* p = (char*)some_const_char_ptr; // const除去か再解釈か不明

// 良い例: constを外す意図が明確
// char* p = const_cast<char*>(some_const_char_ptr);

補足: 安全な型変換にはstatic_castdynamic_castを使います。

reinterpret_castは強引な変換で、原則避けます。

本記事では詳細は割愛します。

最終手段としてのチェックリスト

const_castを使う前に必ず確認したい項目です。

  • もともとのオブジェクトは非constとして確保されているか(リテラルやグローバルconstではないか)。
  • 呼び出す関数は本当に書き換えないか(将来の変更リスクも考慮)。
  • 代わりにconst対応のオーバーロードやラッパーを作れないか。
  • コピーして処理するコストは許容できないか(多くは許容可能)。
  • マルチスレッドや最適化による見え方の問題がないか。

1つでも不安があるなら、const_castは避けるのが賢明です。

まとめ

const_castは「外見上のconstを外す」だけで、オブジェクトの本質的な不変性まで安全に変えられるわけではありません。

元が非constであることが明確な場合に限り、限定的に使用するべき道具です。

レガシーAPI対応やメンバー関数の実装共有など、正当な用途は存在しますが、constで定義されたものを書き換えるのは未定義動作であり、クラッシュや不可思議な動作の温床になります。

日常的には、const対応のAPI設計、コピーによる安全な変更、mutableによるキャッシュ管理などの代替策を選び、どうしても必要なときだけconst_castを明示的に使いましょう。

「コンパイルが通る」は安全の証明ではありません。

この記事を書いた人
エーテリア編集部
エーテリア編集部

C++をこれから学ぶ方に向けて、基礎的な文法や標準ライブラリの使い方を紹介します。モダンな書き方も初心者に合わせてやさしく説明しています。

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

URLをコピーしました!