閉じる

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

C++のconst_castは、オブジェクトの読み取り専用性(const)やハードウェアアクセスの最適化抑止(volatile)といった修飾子を付け外しする特殊なキャストです。

基礎から文法、使いどころ、そして未定義動作に陥らないための注意点までを段階的に解説し、ポインタと参照の具体例、レガシーAPIと付き合う際の判断基準、mutableを用いた安全な代替も示します。

C++のconst_castとは 基礎とルール

const性の基礎

C++では「変更してよいか」を型で表現できます。

constは「値を書き換えてはいけない」契約を表し、コンパイラはこの契約を守るコードかどうかを静的にチェックします。

少し紛らわしいのは、ポインタや参照と組み合わさったときの「どこがconstなのか」という点です。

代表的な例を挙げます。

  • const int x は整数x自体が不変です。
  • int* p は「可変なintを指す可変なポインタ」です。
  • const int* p は「不変なintを指す可変なポインタ」です(指す先は読み取り専用)。
  • int* const p は「可変なintを指す不変なポインタ」です(ポインタ値は固定)。
  • const int* const p は「不変なintを指す不変なポインタ」です。

参照(T&/const T&)も同様に、constは「参照先のオブジェクトを変更しない」という約束を表します。

さらに言うと、constには「トップレベル」と「ボトムレベル(または低レベル)」があります。

たとえばint* constconstはトップレベル(ポインタ自体が不変)で、const int*constはボトムレベル(指す先が不変)です。

この区別は、どのキャストで何が変えられるかを理解する助けになります。

const_castの文法

const_castは次の形で使います。

  • const_cast<新しい型>(式)

ここでの「新しい型」は、元の型と比べてconstvolatileが付いたり外れたりした型でなければなりません。

つまり、const_castの役目はcv修飾子(cvはconst/volatile)の付加や除去だけです。

型そのもの(例: intdoubleへ)を別物に変えることはできません。

  • 付加も除去も可能です。
  • ただし、constを外して書き込んでよいのは「元のオブジェクトがもともと非constだった場合」に限られます。元からconstとして定義されたオブジェクトへ書き込むと未定義動作です。

static_castとの違い

static_castは数値変換や継承階層内の安全なポインタ変換など、言語が定める「静的に安全」とされる変換に使います。

static_castではconstを外せません。

次の表に要点を整理します。

キャスト主な目的const/volatileの付加・除去注意点
const_castcv修飾子の付加・除去可能const int* -> int*元が非constでないと書換えは未定義
static_cast数値変換/明示的な標準変換/継承の安全な変換不可(外せない)double -> intDerived* -> Base*cv修飾子は保持される

コンパイルエラーになる例(参考)です。

static_castでconstを外そうとするとエラーになります。

C++
// このコードは意図的にコンパイルエラーを示すための例です(実行しません)。
const int* p = nullptr;
// int* q = static_cast<int*>(p); // エラー: constを外せない

const_castの使い方 ポインタと参照

ポインタからconstを外す例

「元のオブジェクトが非const」で、途中でconstとして扱っているだけの状況では、const_castで書き込みが可能です。

C++
#include <iostream>

int main() {
    int x = 42;
    const int* p = &x;                // xは非constだが、p経由では読み取り専用の扱い
    int* q = const_cast<int*>(p);     // constを外す
    *q = 100;                         // 元のxが非constなので書き換えは安全

    std::cout << "x=" << x
              << ", *p=" << *p
              << ", *q=" << *q << '\n';
}
実行結果
x=100, *p=100, *q=100

この例ではx自体は非constであり、constなのは「見かけ上(p経由)」だけなので、q経由の書き換えは定義済みで安全です。

参照からconstを外す例

参照でも同じです。

const T&として受け取っても、参照先が実際は非constなら書き換え可能です。

C++
#include <iostream>

// 受け取った参照が実際は非constであることが前提
void bump(const int& r) {
    int& w = const_cast<int&>(r); // constを外す
    w += 10;                      // 元が非constなら安全
}

int main() {
    int v = 5;
    std::cout << "before: v=" << v << '\n';
    bump(v); // vは非constなので安全
    std::cout << "after : v=" << v << '\n';
}
実行結果
before: v=5
after : v=15

関数側は「本当に非constが来るのか」を呼び出し側の契約に依存します。

間違ってconst intオブジェクトを渡すと未定義動作になります。

後述の安全条件を必ず確認してください。

constを付ける使い方

const_castconstを「付ける」こともできます。

ただし、非constをconstとして扱う変換は暗黙変換で可能なため、多くの場合はconst_castすら不要です。

オーバーロード解決で「const版の関数をわざと選びたい」といった意図を明確にする用途で使うことはあります。

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

void f(std::string&) {
    std::cout << "non-const overload\n";
}
void f(const std::string&) {
    std::cout << "const overload\n";
}

int main() {
    std::string s = "abc";
    f(s); // 非const参照版が選ばれる
    f(const_cast<const std::string&>(s)); // 明示的にconst参照版を選ぶ
}
実行結果
non-const overload
const overload

実務ではstd::as_const(s)(C++17)などのほうが意図が明確で安全です。

const_cast<const T&>(obj)は「constを付ける」だけなら冗長であり、過度な使用は可読性を下げます。

const_castを安全に使う条件と注意点

元が非constなら変更可

const_castで書き込みが許されるのは、「元のオブジェクトが非constとして生成され、現在たまたまconst経由で見えているだけ」の場合です。

配列要素や動的確保したオブジェクトでも同様です。

次の関数は、非constを指すconst int*だけを受け取る前提で実装されています。

const intを渡してはいけません。

C++
#include <iostream>

void set_to_zero(const int* p) {
    int* w = const_cast<int*>(p);
    *w = 0; // 元が非constであることが前提
}

int main() {
    int a = 123;
    set_to_zero(&a); // 安全
    std::cout << "a=" << a << '\n';

    // const int b = 456;
    // set_to_zero(&b); // 未定義動作になるため、呼び出してはいけない
}
実行結果
a=0

レガシーAPIに渡す判断基準

C言語由来のレガシーAPIには、歴史的な事情でchar*を受け取るが実際には書き込まない関数が存在します。

この場合、const char*しか持っていない呼び出し側はconst_cast<char*>して渡したくなることがあります。

  • ドキュメントで「書き込まない」と明言されている場合のみ許す。
  • 書き込みの可能性が少しでもあるなら、可変バッファのコピーを渡す。
  • ラッパーを用意し、危険なconst_castは1箇所に閉じ込める。

例として「絶対に書き込まない」ことが仕様で保証されているレガシー関数を想定します。

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

// レガシーAPIの仮想例: 文字列長を返すだけで書き込まない
std::size_t legacy_len(char* s) {
    return std::strlen(s); // 触るだけ
}

int main() {
    std::string s = "hello";
    const char* cs = s.c_str(); // 読み取り専用ビュー

    // 「legacy_lenが書き込まない」ことが仕様で保証されている前提でのみ許す
    std::size_t n = legacy_len(const_cast<char*>(cs));

    std::cout << "length=" << n << '\n';
}
実行結果
length=5

注意点として、std::string::c_str()から得たポインタを書き込みに使うのは規格上未定義です。

ここではあくまで「書き込まないAPIに限って読み取り目的で一時的に型だけ合わせている」点が肝要です。

少しでも不安がある場合は、std::vector<char>std::stringの実体バッファ(非constのdata()。C++17以降)に内容をコピーしてから渡すほうが安全です。

thisを書き換える代わりにmutable

constメンバ関数の内部でキャッシュを更新したい場面があります。

このときconst_castthisのconstを外すのは避けるべきです。

代わりに、更新したいメンバだけをmutableにします。

C++
#include <iostream>

class LazyValue {
    int base_;
    mutable bool cached_ = false; // constメンバ関数からも更新できる
    mutable int cache_ = 0;

public:
    explicit LazyValue(int base) : base_(base) {}

    int value() const {
        if (!cached_) {
            // 高価な計算を模倣
            cache_ = base_ * 2;
            cached_ = true;
            std::cout << "[compute]\n";
        }
        return cache_;
    }

    // const_castでthisを書き換えるのは避けるべき(悪例)
    // int value_bad() const {
    //     auto* self = const_cast<LazyValue*>(this); // 悪例
    //     if (!self->cached_) { ... }
    //     return self->cache_;
    // }
};

int main() {
    LazyValue v(21);
    std::cout << v.value() << '\n'; // ここで計算
    std::cout << v.value() << '\n'; // キャッシュ利用
}
実行結果
[compute]
42
42

mutableは「論理的にはconstだが技術的には変更が必要」という意図を明確に表現でき、const_castよりも安全で可読性が高くなります。

const_castの危険性と未定義動作

本当にconstなオブジェクトの変更は危険

「元からconstとして定義されたオブジェクト」へ書き込むと未定義動作です。

未定義動作とは、クラッシュするかもしれないし、何事もないように見えるかもしれないが、プログラムの正しさが保証されないことを意味します。

C++
// 未定義動作の例(警告: 実行しないこと)
const int y = 5;
const int* p = &y;
int* q = const_cast<int*>(p);
*q = 7; // 未定義動作: yは本当にconst

このようなコードは、最適化で消される、実行時例外が起きる、読み出しても値が変わらないなど、あらゆる振る舞いがあり得ます。

文字列リテラルの書き換えは未定義動作

文字列リテラルは多くの処理系で読み取り専用領域に配置されます。

const_castして書き込むのは未定義動作です。

C++
// 未定義動作の例(警告: 実行しないこと)
char* p = const_cast<char*>("hello");
p[0] = 'H'; // 未定義動作

書き換えたい場合は、書き換え可能なバッファを使います。

C++
#include <iostream>

int main() {
    char buf[] = "hello"; // 配列(可変)
    buf[0] = 'H';
    std::cout << buf << '\n';
}
実行結果
Hello

std::stringも安全な選択肢です。

std::string s = "hello"; s[0] = 'H';のように使えます。

volatileを外すリスク

volatileは最適化による読み書きの省略や並べ替えを抑止し、主にメモリマップトI/Oやシグナルハンドラ、特殊なフラグ共有で使われます。

volatileconst_castで外して読み書きすると、コンパイラが読みを省略してしまい、デバイスの状態変化を見落とす、待機ループが永遠に回り続けるといった不具合に直結します。

C++
// 説明用の擬似コード(実行しません)
extern volatile bool device_ready;

void wait_ready_bad() {
    // 悪例: volatileを外すと、ループが最適化で潰れてしまう可能性がある
    const bool* p = reinterpret_cast<const bool*>(&device_ready);
    while (!*p) { /* ... */ } // 永遠にtrueにならない可能性
}

ハードウェアや並行性に関わる領域では、volatileの意味を理解し、安易に外さないでください。

C++での並行プログラミングには、基本的にstd::atomicを使うべきです。

チェックリストとベストプラクティス

const_castを使う前に、次の観点を順に確認すると安全です。

  • その書き込み対象は「元から非const」か。constとして定義されたオブジェクトではないか。
  • const経由に見えているだけ」の状況に限定されているか。所有者やライフタイムは明確か。
  • 代替手段はないか。API設計の見直し、オーバーロード、std::as_constmutable、コピーして渡すなど。
  • レガシーAPIに渡すとき、ドキュメントで「絶対に書かない」ことが保証されているか。ラッパーに閉じ込められるか。
  • volatileを外していないか。I/Oや並行性の問題を生まないか。
  • 可読性と意図は十分に明確か。コメントで前提条件(元が非constであること等)を残しているか。

ベストプラクティスとしては、const_castは最小限にとどめ、局所化し、十分なコメントとテストで前提を守ることです。

設計段階でconst正当性(const-correctness)を保つことが、結果的にconst_castの必要性を大幅に減らします。

まとめ

const_castは、C++の型システムにおけるconst/volatile修飾子の付け外しに限定された強力な道具です。

正しく使えば、非constな実体を一時的にconstとして扱っているだけの経路で更新したい、といった現実的な要件を安全に満たせます。

しかし、元からconstなオブジェクトや文字列リテラル、volatileに関わるメモリへ書き込むことは未定義動作であり、システムの安定性を損ないます。

レガシーAPIとの橋渡しでは、仕様の確認とラップによる局所化、あるいはコピーの提供が鍵です。

設計段階でconst正当性を徹底し、mutableなどの言語機能を適切に使うことで、const_castの出番は最小限に抑えられます。

使わずに済む設計を第一に、やむを得ず使うときは「元が非constか」を合言葉に、安全条件を満たすことを確認してください。

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

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

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

URLをコピーしました!