このページはC++20に採用された言語機能の変更を解説しています。
のちのC++規格でさらに変更される場合があるため関連項目を参照してください。
概要
C++20より、volatile
の本来の役割に照らして不正確、あるいは誤解を招く用法や無意味な用法について非推奨とされるようになる。
非推奨となるのは次のもの
volatile
値に対する複合代入演算子(算術型・ポインタ型のみ)- C++23で非推奨化解除
volatile
値に対するインクリメント/デクリメント演算子(算術型・ポインタ型のみ)- 間に
volatile
値がある場合の連鎖した代入演算子(非クラス型のみ) - 関数引数のトップレベル
volatile
修飾 - 関数戻り値型のトップレベル
volatile
修飾 - 構造化束縛宣言の
volatile
修飾 std::tuple, std::variant
関連のクラステンプレートのvolatile
特殊化- ロックフリーではない
std::atomic
特殊化のvolatile
メンバ関数
非推奨とされるだけで削除されてはいないが、おそらくコンパイラはそれらの用法について警告を発するようになる。そのような用法はバグの原因となり危険であるため可能な限り使用を避けるべきである。
volatile
volatile
な変数(メモリ領域)への1度のアクセスは正確に1度だけ行われる必要があり、0回にも2回以上にもなってはならない。そして、volatile
領域へのアクセスはその順序がコード上の順序と一致する必要がある。
volatile
の効果(保証)は単純にはこれだけである。
ただし、volatile
はそのようなメモリアクセスが分割されない事は保証していない。volatile
メモリ領域の個々のバイトに対しては正確に1度のアクセスが保証されるが、volatile
領域全体を見たときにアクセスが1度だけになるとは限らない。
そして、非volatile
領域とvolatile
領域へのアクセスの間の相対的な順序が前後しない事まで保証していない。すなわち、volatile
変数へのアクセスと通常の変数へのアクセスは順番が入れ替わりうる。
また、volatile
はマルチスレッドプログラムの実行順を規定するC++メモリモデルとは直接的な関係性を持たず、volatile
領域へのアクセス順序とはC++メモリモデルにおける観測可能な順序を意味しない。プロセッサはC++コード上での順序で読み取ったvolatile
領域へのアクセス命令を、アウトオブオーダーで発行・実行することができる。C++メモリモデルにおいて動作が保証されている同期機構を用いない場合、あるコアにおける命令の実行順は、他のコア(あるいはプロセッサの外部)からは異なった順序で実行されたかのように観測されうる。
volatile
は主として、プログラムの実行環境のハードウェアなどのプログラム外部の環境との通信手段の一つとして利用され、スレッド間のやりとりなどプログラム内部での通信の手段としては適さない。そのようなvolatile
の正しい用法によるメモリの読み書きは、他のどの手段よりも移植性があり機能的にも優れており、言語機能として有用なものである。
コア言語における非推奨化
複合代入演算子、インクリメント演算子
複合代入演算子とは*= /= %= += -= >>= <<= &= ^= |=
の10個の演算子のことで、ある操作とその結果の代入をまとめて行うような演算子のことである。
複合代入演算子およびインクリメント演算子++
とデクリメント演算子--
は、「読み出し - 更新(処理) - 書き込み」という3つの操作を1文で行う。(簡素化のため、以降は++
/--
をまとめてインクリメント演算子と表記する。)
volatile int a = 0;
int b = 10;
a += b;
// これは以下と等価
// int tmp = a;
// a = tmp + b;
++a;
// int tmp = a;
// a = tmp + 1;
a--;
// int tmp = a;
// a = tmp - 1;
複合代入演算子の左辺にあるvolatile
変数、およびインクリメント演算子のvolatile
オペランドには2回のアクセス(読み込みと書き込み1回づつ)が発生するが、このアクセスは複合代入演算子やインクリメント演算子の見た目や一般的な理解とは必ずしも一致しない。
volatile
変数においてはそのアクセス(読み書き)が重要であり、コード上での1回のアクセスは実行時にも1回だけアクセスされる必要がある。しかし、複合代入演算子およびインクリメント演算子のアクセス回数は多くのプログラマにとって曖昧であるか、誤解されている。
従って、算術型・ポインタ型のvolatile
変数に対する組み込みの複合代入演算子およびインクリメント演算子の使用はバグの元であるので、非推奨とされる。
この場合、これらの複合的な演算子を用いず、明示的に「読み出し - 更新 - 書き込み」を分けて書くことでvolatile
変数へのアクセスをコード上でも明確にする事が推奨される。
ただし、複合代入演算子についてはC++23で非推奨化が解除された。
連鎖した代入演算子
代入演算子の一部の用法には、複合代入演算子・インクリメント演算子と同様の問題がある。
volatile int a, b, c;
a = b = c = 10;
// このような順序でアクセスが発生する
// c = 10;
// b = c;
// a = b;
このような連なった代入演算子の用法においては、どの変数にどんな順番で何回アクセスされるのかが非常に分かりづらくなる。
volatile
変数においてはそのアクセス(読み書き)が重要であり、コード上での1回のアクセスは実行時にも1回だけアクセスされ、かつその順番が前後してはならない。しかし、代入演算子を連鎖させた場合、そのアクセス回数および順序は非常に認識しづらくなる。
従って、非クラス型のvolatile
変数に対するこのような代入演算子の使用はバグの元であるので、非推奨とされる。
ただし、非推奨となるのは代入演算子の両端のオペランド以外にvolatile
変数が表れるケースである。
volatile int v1, v2, v3;
v1 = v2 = v3 = 10; // NG(非推奨)
int n;
v1 = n = 10; // OK
v1 = n = v3 = 10; // NG(非推奨)
v3 = 10; // OK
v1 = v3; // OK
v1 = n = v3; // OK
関数引数と戻り値型
関数引数をvolatile
修飾することは、シグナルやsetjmp/longjmp
によって外部から変更されている可能性を示唆するために有効であり、引数のconst
修飾同様に関数定義内では明確な意味を持つ。
一方呼び出し側から見ると、参照・ポインタではないvolatile
引数の意味は不明瞭である。参照・ポインタではない関数引数がvolatile
修飾されている場合、その関数はシグナルハンドラやsetjmp/longjmp
と共に使用されるはずであり、呼び出し側にもそのような実装詳細の一部が漏洩してしまう。
また、呼出規約によっては一部の引数を配置するレジスタがvolatile
となる事があるが、呼出規約はC++コード上で意味を持たず、そのような呼び出し規約がマークされている関数は非volatile
関数宣言と同様に扱われる。しかし、一部のレジスタがvolatile
である事はABIによって処理されている。
このように、関数引数のvolatile
修飾は有用ではないため非推奨とされる。関数の引数をvolatile
としたい場合、関数内でvolatile
ローカル変数に引数をコピーする事が推奨される。一部の実装では、そのようなコピーは省略され、オーバーヘッドとはならない。
void f1(volatile int n); // NG(非推奨)
void f2(volatile int* p); // OK
void f3(volatile int& r); // OK
void f4(int volatile * p); // OK
void f5(int volatile & r); // OK
void f6(int * volatile p); // NG(非推奨)
また、参照・ポインタではない関数戻り値型のvolatile
修飾は完全に意味を持たない。
例えば、ローカルvolatile
変数を返す場合、そのアクセスは関数リターン時に値をコピーするために一度実行されるが、コピーした後の値はもはや元のvolatile
領域とは別のスタック領域にある。volatile
において重要なのは特定領域へのアクセスであり、暗黙にそのようなコピーが行われる事はほとんどの場合にプログラマの意図とは異なる。
ローカルの非volatile
変数をvolatile
として返すことには意味がない。戻り値をvolatile
領域に配置したい場合、関数の呼び出し側でvolatile
変数に受ければよい。
そして、戻り値型のvolatile
修飾は容易に無視する事ができる。
このように、volatile
戻り値型は無意味であるため、非推奨とされる。戻り値をvolatile
として扱いたい場合は、戻り値をvolatile
変数に受ければよい。
volatile int f1(); // NG(非推奨)
volatile int* f2(); // OK
volatile int& f3(); // OK
int volatile* f4(); // OK
int volatile& f5(); // OK
int* volatile f6(); // NG(非推奨)
ただし、関数引数・戻り値型いずれにしても、ポインタ・参照へのvolatile
修飾は明確な意味を持ち有用である(値ではなく、領域にvolatile
とマークしているため)。従って、非推奨とされるのは関数引数・戻り値型へのトップレベルvolatile
修飾のみであって、volatile
ポインタ・参照型は依然として許可される。
構造化束縛宣言
構造化束縛宣言にもvolatile
修飾を行う事ができるが、ここでのCV修飾は右辺にある式の結果である暗黙のオブジェクトに対して作用している。
右辺の式の結果がstd::tuple/std::pair
等のtuple-like
な型のオブジェクトである場合、構造化束縛はまずその結果オブジェクトをvolatile
修飾して受けておき、その結果オブジェクトに対してstd::get
で要素の取得を行う。しかし、std::get
にはvolatile
オーバーロードが欠けており、コンパイルエラーを起こす。
一方、構造化束縛の残りのケース(配列・構造体)の場合はstd::get
を用いないためこのような問題は起こらない。
auto f() -> std::tuple<int, int, double>;
volatile auto [a, b, c] = f(); // NG
// ここでは以下の様な事が行われている
// volatile auto tmp = f();
// std::tuple_element_t<0, decltype(tmp)> a = std::get<0>(tmp);
int array[3]{};
volatile auto [a, b, c] = array; // OK
// ここでは以下の様な事が行われている
// volatile int tmp[] = {array[0], array[1], array[2]};
// volatile int a = tmp[0];
static_assert(std::is_volatile_v<decltype(a)>); // OK
このような挙動の非一貫性を受け入れたとしても、構造化束縛の裏で行われている事がvolatile
には適していない。
構造化束縛のvolatile
修飾はその右辺にある暗黙のオブジェクトに対して行われるが、その事は構文からは完全に隠蔽されている。右辺の式の結果オブジェクトも場合によってコピーされたり参照のまま利用されたりと、扱いが変化しうる。また、構造化束縛宣言に指定した変数名はコンパイラの扱いとしては変数名ではなく、右辺の暗黙のオブジェクト内の対応する要素にバインドされた名前でしかない。そのような名前に対するvolatile
の効果は不明瞭であり、束縛先の要素の型がvolatile
ではない場合には意味をなさない。
volatile
においてはその領域へのアクセスが重要であり、1度のアクセスは正確に1度だけ行われる必要があり、その順序は前後してはならない。構造化束縛宣言はその裏側で多くの事が行われており、それはCV・参照修飾と右辺の式の結果型によって様々に変化するが、そこでどのオブジェクトがvolatile
となりどのような順番でアクセスが発生するのかは非常に不明瞭である。
従って、構造化束縛宣言のvolatile
修飾を正しく扱う事は非常に困難であるため、非推奨とされる。
構造化束縛した名前がvolatile
である必要がある場合は、分解対象の右辺の結果オブジェクトの各要素型をあらかじめvolatile
修飾しておく事が推奨される。
auto f() -> std::tuple<int, int, double>;
int array[3]{};
volatile auto [a, b, c] = f(); // NG(非推奨)
volatile auto [a, b, c] = array; // NG(非推奨)
auto g() -> std::tuple<volatile int*, volatile int*, volatile double&>;
auto [a, b, c] = g(); // OK
この場合でも、各要素型のトップレベルvolatile
修飾は意味をなさない。
auto f() -> std::tuple<volatile int, volatile int, volatile double>;
auto [a, b, c] = f(); // OK、要素ごとコピーされている、volatile修飾は無意味
auto&& [a, b, c] = f(); // OK、一時オブジェクト内各要素へのバインド、volatile修飾は無意味
ライブラリにおける非推奨化
クラステンプレートのvolatile
な特殊化
標準ライブラリのクラステンプレートには、volatile
な対象に対する特殊化が提供されているものがある。
このうち、numeric_limits
に関しては有用性が明らかであるのでそのままにされ、atomic
関連のものはあとで説明する。
残ったのはtuple
とvariant
に関連したものであるが、C++標準はtuple
とvariant
がどのように実装されるかを指定しておらず、これらの型のvolatile
オブジェクトへのアクセスがどのように振る舞うのかは不透明である。さらに、メンバ関数は特にvolatile
修飾されたものが用意されているわけではなく、標準ライブラリのそのほかのクラスも特にvolatile
を意識してはいない。
従って、tuple
とvariant
そのものはvolatile
で使用されるために設計されておらず、これら4つのクラステンプレートのvolatile
特殊化はCV修飾を網羅するために用意されているだけであるため、非推奨とされる。
std::atomic
のvolatile
メンバ関数
volatile
なアクセスは不可分ではなく、順序保証がなく、各バイトに正確に一度だけアクセスされ、コンパイラの最適化の対象とならない。
atomicなアクセスは不可分であり(原子性が保証され)、C++メモリモデルによってその順序が保証され、ループによって処理される可能性があり(各バイト一度だけのアクセスは保証されない)、コンパイラの最適化の対象となりえる。
volatile std::atomic<T>
はこれらの性質を組み合わせたものとなる事が期待されるが、そうなっていない。
ロックフリーではないstd::atomic
がvolatile
修飾されている場合、そのアクセスの不可分性(原子性)が必ずしも保証されない。例えば、C++プログラム上ではmutex
などでロックして正しくatomicにアクセスされていたとしても、その領域にアクセスするプログラム外部の観測者からはそのアクセスは分割されて見えうる。
また、volatile
かつatomicな複合代入操作を正しく行う(不可分に、各バイトに正確に一度だけアクセスする)実装は限られているが、そのような実装詳細を指定することはC++標準の範囲内ではない。
従って、volatile std::atomic<T>
は必ずしも両方の性質を備えた実装になっておらず、それを保証する事が困難であるため、std::atomic<T>::is_always_lock_free
がtrue
となる特殊化を除いてstd::atomic
のvolatile
メンバ関数は非推奨とされる。
検討されたほかの選択肢
関数引数・戻り値型へのconst
修飾
関数引数・戻り値型へのvolatile
修飾が非推奨とされたのとほぼ同様の理由によって、const
修飾も非推奨とする事が提案されていたが、合意が取れなかったため非推奨とはならなかった。
おそらく、間違っていたり意味がなかったとしても、volatile
と比べて幅広く使用されているために非推奨とする事が忌避されたものと思われる。
メンバ関数のvolatile
修飾
メンバ関数のvolatile
修飾は通常使用されることはない。標準ライブラリにおいても、const
メンバ関数は意図を持って用意される事がある一方で、volatile
メンバ関数が用意されることはほとんどない。
また、メンバ関数のvolatile
修飾はジェネリックプログラミングにおいてはCV修飾のパターン網羅のために追加の負担をかけるか、全く無視されるかのどちらかである。
クラスはconst
な文脈でもそうでない文脈でも使用可能であり、const
メンバ関数はそれぞれの場合に合わせてその振る舞いを変化させる事ができる。volatile
の場合を考えてみると、これは当てはまらない。
あるクラスがvolatile
な領域に配置されることもあればそうでない場合もある、という状況は考えづらく、そのような状況にあったとして、メンバ関数がどう有意義に異なるのかはさらに不明瞭である。
さらに、コンストラクタやデストラクタはconst
もvolatile
修飾もできないため、クラスのオブジェクトへのvolatile
性が有効になるのは派生先も含めてコンストラクタの呼び出しが完了した後からとなる(同様に、volatile
性はデストラクタの呼び出し前までしか有効ではない)。
volatile
においてはその領域へのアクセスが重要であり、1度のアクセスは正確に1度だけ行われる必要があり、その順序は前後してはならない。クラスのオブジェクトがvolatile
な領域に配置されるとき、volatile
の保証なしで構築や破棄をすることは間違っている。クラスオブジェクトがvolatile
な領域に配置される場合は、そのメンバ変数をvolatile
修飾しておく事が望ましい。
このように、メンバ関数のvolatile
修飾は無意味なものであり、使用されていないため、非推奨とする事が提案されていた。
しかし、集成体のような単純なクラス型においては、クラスオブジェクトのvolatile
領域への配置とメンバ関数のvolatile
修飾は使用されている。これに対処するために当初の提案では、メンバ関数のvolatile
修飾はあるかないかどちらかとする、struct volatile
を導入する、などの回避案を提案していた。
また、std::atomic
は意図を持ってvolatile
修飾されたメンバ関数を提供しており、それは有用であるため、これを非推奨としてしまうかどうかも問題となった。
結局、合意に至ることはできず、メンバ関数のvolatile
修飾は現状維持となった。
備考
非推奨化で触れられてはいないが、volatile
変数を並行処理の共有変数として使用することは常に間違っている。
不適切な使用の例
提案文書より不適切と思われるvolatile
の用例をいくつか引用する。中には今回の非推奨化の対象となっていないものもある。
struct foo {
int a : 4;
int b : 2;
};
volatile foo f;
// どんな命令が生成され、fの領域の各バイトに何回アクセスするか不透明
f.a = 3;
struct foo {
volatile int a : 4;
int b : 2;
};
foo f;
// f.aの領域へのアクセスが発生するか不透明
f.b = 1;
union foo {
char c;
int i;
};
volatile foo f;
// sizeof(int) [byte]の領域へアクセスするか、sizeof(char) [byte]の領域へアクセスするのか、不透明
f.c = 42;
volatile int i;
// iの領域へ何回のアクセスが発生するか不透明
// これはどちらも非推奨化
i += 42; // C++23で非推奨化解除
++i;
volatile int i, j, k;
// iへの代入時にjの値を読み取るか不透明(非推奨化)
i = j = k;
struct big { int arr[32]; };
volatile _Atomic struct big ba;
struct big b2;
// 誰から見てもatomicになるとは限らない
// ほとんどの環境で非推奨化
ba = b2;
int what(volatile std::atomic<int> *atom) {
int expected = 42;
// この操作でatomの指す領域に何回アクセスが発生するのか不透明(場合によって変化する)
atom->compare_exchange_strong(expected, 0xdead);
return expected;
}
// この関数を呼び出すとき、呼び出し側は何を気にすべきか不透明
void what_does_the_caller_care(volatile int);
// 無意味(非推奨化)
volatile int nonsense(void);
struct retme { int i, j; };
// 無意味(非推奨化)
volatile struct retme silly(void);
struct device {
unsigned reg;
device() : reg(0xc0ffee) {}
~device() { reg = 0xdeadbeef; }
};
// 初期化(コンストラクタ内)、破棄(デストラクタ内)はともにvolatileではない
volatile device dev;
参照
- P1152R0 Deprecating
volatile
- P1152R1 Deprecating
volatile
- P1152R2 Deprecating
volatile
- P1152R4 Deprecating
volatile
- P1831R0 Deprecating
volatile
: library - P1831R0 Deprecating
volatile
: library - P2327R0 De-deprecating volatile compound assignment
- CWG Issue 2654. Un-deprecation of compound volatile assignments