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* const
のconst
はトップレベル(ポインタ自体が不変)で、const int*
のconst
はボトムレベル(指す先が不変)です。
この区別は、どのキャストで何が変えられるかを理解する助けになります。
const_castの文法
const_cast
は次の形で使います。
const_cast<新しい型>(式)
ここでの「新しい型」は、元の型と比べてconst
やvolatile
が付いたり外れたりした型でなければなりません。
つまり、const_cast
の役目はcv修飾子(cvはconst/volatile)の付加や除去だけです。
型そのもの(例: int
をdouble
へ)を別物に変えることはできません。
- 付加も除去も可能です。
- ただし、constを外して書き込んでよいのは「元のオブジェクトがもともと非constだった場合」に限られます。元から
const
として定義されたオブジェクトへ書き込むと未定義動作です。
static_castとの違い
static_cast
は数値変換や継承階層内の安全なポインタ変換など、言語が定める「静的に安全」とされる変換に使います。
static_cast
ではconst
を外せません。
次の表に要点を整理します。
キャスト | 主な目的 | const/volatileの付加・除去 | 例 | 注意点 |
---|---|---|---|---|
const_cast | cv修飾子の付加・除去 | 可能 | const int* -> int* | 元が非constでないと書換えは未定義 |
static_cast | 数値変換/明示的な標準変換/継承の安全な変換 | 不可(外せない) | double -> int 、Derived* -> Base* | cv修飾子は保持される |
コンパイルエラーになる例(参考)です。
static_cast
でconstを外そうとするとエラーになります。
// このコードは意図的にコンパイルエラーを示すための例です(実行しません)。
const int* p = nullptr;
// int* q = static_cast<int*>(p); // エラー: constを外せない
const_castの使い方 ポインタと参照
ポインタからconstを外す例
「元のオブジェクトが非const」で、途中でconst
として扱っているだけの状況では、const_cast
で書き込みが可能です。
#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なら書き換え可能です。
#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_cast
はconst
を「付ける」こともできます。
ただし、非constをconstとして扱う変換は暗黙変換で可能なため、多くの場合はconst_cast
すら不要です。
オーバーロード解決で「const版の関数をわざと選びたい」といった意図を明確にする用途で使うことはあります。
#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
を渡してはいけません。
#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箇所に閉じ込める。
例として「絶対に書き込まない」ことが仕様で保証されているレガシー関数を想定します。
#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_cast
でthis
のconstを外すのは避けるべきです。
代わりに、更新したいメンバだけをmutable
にします。
#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として定義されたオブジェクト」へ書き込むと未定義動作です。
未定義動作とは、クラッシュするかもしれないし、何事もないように見えるかもしれないが、プログラムの正しさが保証されないことを意味します。
// 未定義動作の例(警告: 実行しないこと)
const int y = 5;
const int* p = &y;
int* q = const_cast<int*>(p);
*q = 7; // 未定義動作: yは本当にconst
このようなコードは、最適化で消される、実行時例外が起きる、読み出しても値が変わらないなど、あらゆる振る舞いがあり得ます。
文字列リテラルの書き換えは未定義動作
文字列リテラルは多くの処理系で読み取り専用領域に配置されます。
const_cast
して書き込むのは未定義動作です。
// 未定義動作の例(警告: 実行しないこと)
char* p = const_cast<char*>("hello");
p[0] = 'H'; // 未定義動作
書き換えたい場合は、書き換え可能なバッファを使います。
#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やシグナルハンドラ、特殊なフラグ共有で使われます。
volatile
をconst_cast
で外して読み書きすると、コンパイラが読みを省略してしまい、デバイスの状態変化を見落とす、待機ループが永遠に回り続けるといった不具合に直結します。
// 説明用の擬似コード(実行しません)
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_const
、mutable
、コピーして渡すなど。 - レガシー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か」を合言葉に、安全条件を満たすことを確認してください。