最終曎新日時:
が曎新

履歎 線集

ほずんどのvolatileを非掚奚化 [P1152R4](C++20)

このペヌゞはC++20に採甚された蚀語機胜の倉曎を解説しおいたす。

のちのC++芏栌でさらに倉曎される堎合があるため関連項目を参照しおください。

抂芁

C++20より、volatileの本来の圹割に照らしお䞍正確、あるいは誀解を招く甚法や無意味な甚法に぀いお非掚奚ずされるようになる。

非掚奚ずなるのは次のもの

  1. volatile倀に察する耇合代入挔算子算術型・ポむンタ型のみ
    • C++23で非掚奚化解陀
  2. volatile倀に察するむンクリメントデクリメント挔算子算術型・ポむンタ型のみ
  3. 間にvolatile倀がある堎合の連鎖した代入挔算子非クラス型のみ
  4. 関数匕数のトップレベルvolatile修食
  5. 関数戻り倀型のトップレベルvolatile修食
  6. 構造化束瞛宣蚀のvolatile修食
  7. std::tuple, std::variant関連のクラステンプレヌトのvolatile特殊化
  8. ロックフリヌではない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;

参照