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 int
をint
に変換できます。
ただし外見上の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
して渡すと意図が明確になります。
#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であることが条件です。
次は安全なケースです。
#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として定義されたオブジェクトから外すのは未定義動作です。
次は「コンパイルは通るがやってはいけない」例です。
// 実行しないでください: 未定義動作の例
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
を使う方法があります。
#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は変わらない」と仮定して最適化します。
よって観測結果が環境や最適化レベルで変わる、落ちるなどの問題を引き起こします。
// 未定義動作の例(動かさないこと)
#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";
のように配列にコピーすれば変更可能です。
// 悪い例: リテラルの書き換えは未定義動作
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
のまま受け取りましょう。
#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!
コピーして安全に変更する
読み取り専用のものを変更したいならコピーが基本です。
最小限のコストで安全性を買えます。
#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_cast
でthis
を書き換えるのは避けましょう。
#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)expr
やT(expr)
は、複数の種類のキャストが試され、どれが行われたかが見えづらい問題があります。
const
を外すならconst_cast
を明示し、意図と危険性をコードに刻みましょう。
// 悪い例: 何が起きているかわからない
// char* p = (char*)some_const_char_ptr; // const除去か再解釈か不明
// 良い例: constを外す意図が明確
// char* p = const_cast<char*>(some_const_char_ptr);
補足: 安全な型変換にはstatic_cast
やdynamic_cast
を使います。
reinterpret_cast
は強引な変換で、原則避けます。
本記事では詳細は割愛します。
最終手段としてのチェックリスト
const_castを使う前に必ず確認したい項目です。
- もともとのオブジェクトは非constとして確保されているか(リテラルやグローバルconstではないか)。
- 呼び出す関数は本当に書き換えないか(将来の変更リスクも考慮)。
- 代わりにconst対応のオーバーロードやラッパーを作れないか。
- コピーして処理するコストは許容できないか(多くは許容可能)。
- マルチスレッドや最適化による見え方の問題がないか。
1つでも不安があるなら、const_castは避けるのが賢明です。
まとめ
const_castは「外見上のconstを外す」だけで、オブジェクトの本質的な不変性まで安全に変えられるわけではありません。
元が非constであることが明確な場合に限り、限定的に使用するべき道具です。
レガシーAPI対応やメンバー関数の実装共有など、正当な用途は存在しますが、constで定義されたものを書き換えるのは未定義動作であり、クラッシュや不可思議な動作の温床になります。
日常的には、const対応のAPI設計、コピーによる安全な変更、mutable
によるキャッシュ管理などの代替策を選び、どうしても必要なときだけconst_cast
を明示的に使いましょう。
「コンパイルが通る」は安全の証明ではありません。