C++のラムダ式のキャプチャについて

AI実装検定のご案内

C++のラムダ式におけるキャプチャとは、ラムダ式の外側にあるローカル変数を、ラムダ式の内部で使えるようにする仕組みです。

ラムダ式は、単なる無名関数のように見えますが、実際にはコンパイラが生成する「関数呼び出し可能なオブジェクト」として扱われます。

そのため、外側の変数を使いたい場合には、その変数をどのような形でラムダに持たせるのかを指定する必要があります。

その指定を行うのが、ラムダ式の先頭にある角括弧の部分です。

たとえば、外側の変数をコピーして使いたいのか、元の変数を参照して使いたいのかによって、キャプチャの書き方が変わります。

ラムダ式のキャプチャを理解するうえで重要なのは、次の2点です。

1つ目は、値としてキャプチャするのか、参照としてキャプチャするのかという点です。

2つ目は、ラムダが実行される時点で、参照先の変数やオブジェクトがまだ存在しているかという点です。

特にC++では、寿命が切れた変数を参照すると未定義動作になるため、キャプチャの理解は非常に重要です。

目次

値キャプチャとは

値キャプチャの基本

値キャプチャとは、外側の変数の値をラムダ式の中にコピーして保持するキャプチャ方法です。

ラムダ式を作った時点の値がコピーされるため、その後に外側の変数が変更されても、ラムダ内部で見える値は変わりません。

つまり、値キャプチャでは、ラムダは外側の変数そのものを見ているのではなく、作成時点でコピーされた値を見ています。

この性質により、値キャプチャは比較的安全です。

ラムダが外側のスコープより長く生きる場合でも、コピーされた値を使うため、参照切れの問題が起きにくいからです。

値キャプチャの注意点

値キャプチャされた変数は、デフォルトではラムダ内部で変更できません。

これは、値キャプチャされた変数そのものが完全に定数になるというより、ラムダの呼び出し演算子がデフォルトで const として扱われるためです。

そのため、ラムダ内部で値キャプチャしたコピーを変更したい場合は、mutable を使う必要があります。

ただし、mutable を使って変更できるのは、あくまでラムダが内部に保持しているコピーです。

外側の元の変数が変更されるわけではありません。

この点は混同しやすい部分です。

値キャプチャは、外側の値を固定して使いたい場合や、ラムダが後から実行される可能性がある場合に向いています。

参照キャプチャとは

参照キャプチャの基本

参照キャプチャとは、外側の変数をコピーせず、元の変数を参照するキャプチャ方法です。

ラムダ内部でその変数を読み書きすると、外側の変数そのものにアクセスします。

そのため、ラムダを作った後に外側の変数が変更されれば、ラムダ内部で見える値も変わります。

また、ラムダ内部からその変数を変更すれば、外側の変数にも変更が反映されます。

参照キャプチャは、外側の変数をラムダ内から更新したい場合に便利です。

参照キャプチャの注意点

参照キャプチャで最も注意すべき点は、参照先の変数の寿命です。

ラムダが実行される時点で、参照している変数がすでに破棄されている場合、そのラムダを呼び出すと未定義動作になります。

これは、C++のラムダ式で非常によくあるバグの原因です。

特に危険なのは、次のようなケースです。

  • 関数の中でローカル変数を参照キャプチャしたラムダを返す場合
  • コールバックとしてラムダを保存する場合
  • スレッドや非同期処理にラムダを渡す場合
  • ループ変数を参照キャプチャして、あとからラムダを実行する場合

参照キャプチャは便利ですが、ラムダがその場ですぐに実行される短い処理で使うのが基本です。

ラムダが長く生きる可能性がある場合は、値キャプチャを優先した方が安全です。

値キャプチャと参照キャプチャの違い

コピーを持つか、元の変数を見るか

値キャプチャと参照キャプチャの最大の違いは、ラムダがコピーを持つのか、元の変数を参照するのかです。

値キャプチャでは、ラムダ作成時点の値がコピーされます。

そのため、外側の変数が後から変わっても、ラムダ内部の値には影響しません。

一方、参照キャプチャでは、ラムダは外側の変数そのものを参照します。

そのため、外側の変数の変更はラムダにも反映されますし、ラムダ内から外側の変数を変更することもできます。

安全性とコストの違い

値キャプチャは、参照切れのリスクが少ないため安全性が高いです。

ただし、大きなオブジェクトをコピーする場合は、コピーコストが発生します。

参照キャプチャは、コピーコストを避けられる点では有利です。

しかし、参照先の寿命を正しく管理しなければ、未定義動作につながります。

実務では、次のように考えると分かりやすいです。

外側の値を読むだけで、ラムダが後から実行される可能性があるなら、値キャプチャが安全です。

外側の変数を変更したい場合や、コピーコストを避けたい場合は、参照キャプチャを検討します。

ただし、その場合は、参照先の寿命がラムダの実行時点まで確実に続いていることを確認する必要があります。

デフォルトキャプチャ

値によるデフォルトキャプチャ

値によるデフォルトキャプチャは、ラムダ内で必要になった外側のローカル変数を、基本的に値でキャプチャする方法です。

明示的に一つひとつ変数を書かなくても、ラムダ内で使う変数を値として扱えるため便利です。

ただし、「ラムダ内に名前が出てきた変数がすべて必ずコピーされる」と理解するのは少し単純化しすぎです。

厳密には、変数の使われ方によって実際にキャプチャされるかどうかが決まります。

実務的には、「ラムダ内で必要になった外側のローカル変数を値として使える指定」と理解しておけば十分です。

参照によるデフォルトキャプチャ

参照によるデフォルトキャプチャは、ラムダ内で必要になった外側のローカル変数を、基本的に参照でキャプチャする方法です。

短い処理では非常に便利です。

外側の複数の変数をラムダ内で更新したい場合、一つひとつ参照キャプチャを書く必要がありません。

ただし、参照によるデフォルトキャプチャは、何を参照しているのかが分かりにくくなりやすいという問題があります。

ラムダの処理が長い場合や、あとから保存・返却・非同期実行される場合には、参照切れのリスクが高まります。

そのため、実務ではデフォルト参照キャプチャを多用しすぎない方が安全です。

混在キャプチャ

基本は値、一部だけ参照

ラムダ式では、基本的には値キャプチャにしつつ、特定の変数だけ参照キャプチャにすることができます。

これは、計算に使う値はコピーして固定し、結果を書き込む変数だけ外側へ反映したい場合などに便利です。

たとえば、条件や入力値は値として保持し、結果格納用の変数だけ参照で扱うような使い方です。

この方法を使うと、どの変数がコピーされ、どの変数が外側に影響するのかを明確にできます。

基本は参照、一部だけ値

逆に、基本的には参照キャプチャにしつつ、特定の変数だけ値キャプチャにすることもできます。

これは、ほとんどの変数は外側と連動させたいが、一部の値だけはラムダ作成時点の値に固定したい場合に使います。

ただし、基本を参照キャプチャにすると、意図しない変数まで参照で扱われる可能性があります。

そのため、コードの読みやすさや安全性を重視するなら、重要なラムダではキャプチャ対象を明示した方がよいです。

mutableの役割

値キャプチャしたコピーを変更するための指定

ラムダ式では、値キャプチャした変数はデフォルトでは変更できません。

ラムダ内部で値キャプチャしたコピーを変更したい場合に使うのが mutable です。

mutable を付けると、ラムダオブジェクトが内部に保持しているコピーを変更できるようになります。

ただし、ここで変更されるのは外側の変数ではありません。

あくまでラムダ自身が保持している内部状態です。

ラムダに状態を持たせる使い方

mutable は、ラムダ自体に状態を持たせたい場合にも使われます。

たとえば、呼び出すたびに内部カウントを増やすようなラムダを作る場合です。

このようなラムダでは、値キャプチャした変数を内部状態として扱います。

ラムダを呼び出すたびにその内部状態が変わりますが、外側の変数には影響しません。

つまり、mutable は「外側の変数を変更するためのもの」ではなく、「ラムダ内部のコピーを変更するためのもの」と理解すると分かりやすいです。

初期化キャプチャ

初期化キャプチャの基本

初期化キャプチャとは、ラムダ式のキャプチャ時に新しい名前を作り、任意の式で初期化できる機能です。

C++14以降で使えるようになりました。

通常のキャプチャでは外側に存在する変数名をそのまま指定しますが、初期化キャプチャでは、ラムダ内部用の新しい変数名を作ることができます。

たとえば、外側の値を加工した結果をラムダ内に保持したい場合に便利です。

ムーブキャプチャでの利用

初期化キャプチャは、ムーブ専用オブジェクトをラムダに渡すときによく使われます。

代表例が std::unique_ptr です。

std::unique_ptr はコピーできないため、通常の値キャプチャではラムダに持たせることができません。

そのような場合、初期化キャプチャと std::move を組み合わせて、所有権をラムダへ移動します。

この使い方は、非同期処理やリソース管理でよく使われます。

ただし、ムーブ専用オブジェクトをキャプチャしたラムダは、ラムダ自身もコピーできなくなる場合があります。

そのため、std::function に格納できないことがある点には注意が必要です。

thisのキャプチャ

thisキャプチャの基本

クラスのメンバ関数内でラムダ式を使う場合、メンバ変数やメンバ関数にアクセスしたくなることがあります。

その場合に使われるのが this のキャプチャです。

this をキャプチャすると、ラムダ内部から現在のオブジェクトのメンバにアクセスできます。

ただし、重要なのは、this キャプチャはオブジェクト全体をコピーするわけではないという点です。

実際には、現在のオブジェクトを指すポインタをラムダが保持します。

thisキャプチャの注意点

this をキャプチャしたラムダは、元のオブジェクトが存在している間だけ安全に使えます。

もしラムダがオブジェクトより長く生きてしまうと、破棄済みのオブジェクトにアクセスすることになり、未定義動作になります。

これは、コールバックや非同期処理で特に問題になります。

たとえば、オブジェクトのメンバ関数内で作ったラムダを外部に保存し、その後に元のオブジェクトが破棄されると危険です。

this キャプチャは便利ですが、オブジェクトの寿命を意識して使う必要があります。

*thisのキャプチャ

オブジェクト自体をコピーするキャプチャ

C++17以降では、*this をキャプチャすることで、現在のオブジェクト自体をラムダにコピーできます。

これは、this ポインタをコピーするのとは異なります。

this キャプチャでは、ラムダは元のオブジェクトを指すポインタを持ちます。

一方、*this キャプチャでは、ラムダは現在のオブジェクトのコピーを持ちます。

そのため、元のオブジェクトが破棄された後でも、ラムダ内のコピーを使って処理できます。

*thisキャプチャの注意点

*this キャプチャは安全性を高められる一方で、コピーコストが発生します。

また、オブジェクトがコピーできないメンバを持っている場合には使えないことがあります。

さらに、コピーされたオブジェクトは元のオブジェクトとは別物です。

ラムダ内でコピー側の状態を参照しているだけなので、元のオブジェクトの最新状態が反映されるわけではありません。

そのため、this を使うべきか、*this を使うべきかは、オブジェクトの寿命やコピーコスト、状態の同期が必要かどうかを考えて判断する必要があります。

メンバ変数とキャプチャの関係

メンバ変数は直接キャプチャできない

クラスのメンバ変数は、ローカル変数ではありません。

そのため、メンバ変数だけをキャプチャリストに直接書いてキャプチャすることはできません。

メンバ変数をラムダ内で使いたい場合は、基本的に this をキャプチャする必要があります。

または、メンバ変数の値を一度ローカル変数にコピーし、そのローカル変数をキャプチャする方法もあります。

C++14以降であれば、初期化キャプチャを使って、メンバ変数の値をラムダ内の新しい変数として保持することもできます。

デフォルト値キャプチャとメンバ変数

メンバ関数内でデフォルト値キャプチャを使う場合には注意が必要です。

一見すると、メンバ変数が値としてコピーされているように見えることがあります。

しかし、実際にはメンバ変数そのものが直接コピーされるわけではありません。

メンバ変数へのアクセスは、基本的に this 経由のアクセスになります。

そのため、デフォルト値キャプチャを使っていても、元のオブジェクトの寿命が切れた後にラムダを呼ぶと危険です。

C++20以降では、デフォルト値キャプチャによる this の暗黙的なキャプチャは非推奨とされています。

そのため、メンバにアクセスするラムダでは、this*this を明示した方が読みやすく、安全です。

グローバル変数やstatic変数はキャプチャ不要

キャプチャが必要なのは主にローカル変数

ラムダ式のキャプチャが必要になるのは、基本的に外側のローカル変数です。

一方、グローバル変数や static 変数はキャプチャしなくてもラムダ内から使えます。

これは、それらの変数が通常のローカル変数とは異なる記憶域期間を持っているためです。

キャプチャ対象として考えるべきもの

実務的には、キャプチャが必要かどうかを次のように考えると分かりやすいです。

関数内で定義された通常のローカル変数をラムダ内で使う場合は、キャプチャが必要です。

グローバル変数、static 変数、関数などは、基本的にキャプチャなしで使えます。

クラスのメンバ変数は直接キャプチャできないため、this を通してアクセスするか、ローカル変数や初期化キャプチャを使って値を保持します。

ラムダ式の正体

ラムダ式はオブジェクトとして扱われる

ラムダ式は、見た目は関数のようですが、実際にはコンパイラが生成する名前のないクラス型のオブジェクトです。

このオブジェクトは、関数呼び出し演算子を持っており、関数のように呼び出すことができます。

値キャプチャされた変数は、このオブジェクトの内部にメンバのように保持されます。

参照キャプチャされた変数は、外側の変数を参照する形で扱われます。

このように考えると、値キャプチャと参照キャプチャの違いが理解しやすくなります。

キャプチャありラムダは状態を持つ

キャプチャなしラムダは、外部の状態を持たない単純な呼び出し可能オブジェクトです。

一方、キャプチャありラムダは、コピーした値や参照を内部に持つ「状態付き」のオブジェクトです。

そのため、キャプチャありラムダは通常の関数ポインタとは異なります。

キャプチャなしラムダは条件が合えば関数ポインタに変換できますが、キャプチャありラムダは状態を持つため、関数ポインタには変換できません。

std::functionとキャプチャ

キャプチャありラムダを保持するための型

キャプチャありラムダを変数に格納したり、関数から返したりするときに、std::function が使われることがあります。

std::function は、呼び出し可能なものを統一的に扱える便利な型です。

ラムダ式、関数ポインタ、関数オブジェクトなどを同じように保持できます。

キャプチャありラムダも、条件を満たしていれば std::function に格納できます。

std::functionの注意点

std::function は便利ですが、万能ではありません。

まず、型消去によるオーバーヘッドがあります。

そのため、単にローカル変数としてラムダを使うだけなら、auto で受ける方が自然な場合が多いです。

また、std::function に格納する呼び出し可能オブジェクトは、基本的にコピー可能である必要があります。

そのため、std::unique_ptr などのムーブ専用オブジェクトをキャプチャしたラムダは、通常の std::function には入れられない場合があります。

C++23以降では、ムーブ専用の呼び出し可能オブジェクトを扱うための選択肢もありますが、通常のC++ではこの制限を意識する必要があります。

非同期処理とキャプチャ

非同期処理では寿命管理が重要

スレッド、非同期処理、イベントハンドラ、コールバックなどでラムダを使う場合、キャプチャの選び方は特に重要です。

非同期処理では、ラムダがいつ実行されるか分からないことがあります。

そのため、参照キャプチャを使うと、ラムダが実行される時点で参照先の変数がすでに破棄されている可能性があります。

このような場合、未定義動作につながります。

非同期処理では、基本的に値キャプチャを優先するのが安全です。

値キャプチャでも完全に安全とは限らない

ただし、値キャプチャすれば常に安全というわけではありません。

たとえば、ポインタを値キャプチャした場合、コピーされるのはポインタの値だけです。

ポインタが指している先のオブジェクトの寿命までは保証されません。

また、共有オブジェクトを複数スレッドから操作する場合には、データ競合にも注意が必要です。

非同期処理では、単にキャプチャ方法だけでなく、オブジェクトの寿命管理や排他制御も含めて考える必要があります。

shared_ptrとweak_ptrのキャプチャ

shared_ptrを値キャプチャする使い方

非同期処理やコールバックでは、オブジェクトの寿命を延ばすために std::shared_ptr を値キャプチャすることがあります。

ラムダが shared_ptr を保持している間、そのオブジェクトは破棄されません。

そのため、コールバック実行時に対象オブジェクトが消えてしまう問題を避けられます。

これは、GUI、非同期I/O、ゲーム開発、サーバー処理などでよく使われる手法です。

ただし、shared_ptr をキャプチャすると、意図せずオブジェクトの寿命が延び続けることがあります。

weak_ptrを使う場面

shared_ptr の値キャプチャは便利ですが、循環参照を引き起こすことがあります。

そのような場合には、std::weak_ptr をキャプチャする方法があります。

weak_ptr は所有権を持たないため、オブジェクトの寿命を延ばしません。

ラムダ実行時に、対象オブジェクトがまだ生きているかを確認し、生きている場合だけ処理を行うことができます。

この方法は、オブジェクトがすでに破棄されている可能性を安全に扱いたい場合に有効です。

ループ内でのキャプチャの注意点

ループ変数の参照キャプチャは危険

ラムダ式でよくあるミスの一つが、ループ変数を参照キャプチャすることです。

ループの中で複数のラムダを作成し、それらを後から実行する場合、ループ変数を参照キャプチャしていると問題が起こります。

ループ変数は各ラムダごとに別々に保存されるわけではありません。

参照キャプチャでは、同じ変数を参照し続けます。

さらに、ループが終了した後にその変数の寿命が切れている場合、そのラムダを呼び出すと未定義動作になります。

ループでは値キャプチャが安全

ループ内で作ったラムダを後から実行する場合は、ループ変数を値キャプチャするのが基本です。

値キャプチャすれば、各反復時点の値がラムダにコピーされます。

そのため、それぞれのラムダが意図した値を保持できます。

これは、コールバックの登録やタスクの生成でよく重要になります。

キャプチャなしラムダ

外側の変数を使わないラムダ

ラムダ式は、必ずキャプチャを使う必要があるわけではありません。

外側のローカル変数を使わない場合、キャプチャなしラムダとして書けます。

キャプチャなしラムダは、外部状態を持たないため扱いやすく、条件が合えば関数ポインタに変換できます。

キャプチャありラムダとの違い

キャプチャありラムダは、コピーした値や参照を内部に持つため、状態を持ちます。

そのため、通常の関数ポインタには変換できません。

この違いは、C言語由来のAPIにコールバックを渡すときなどに重要です。

C形式の関数ポインタを要求するAPIには、キャプチャありラムダをそのまま渡すことはできません。

キャプチャを使うときの判断基準

読むだけなら値キャプチャを優先する

外側の変数を読むだけであれば、基本的には値キャプチャを優先すると安全です。

値キャプチャなら、ラムダが後から実行されても、コピーされた値を使うため寿命切れの問題を避けやすくなります。

特に、ラムダを返す場合や、コールバックとして保存する場合、非同期処理に渡す場合には、値キャプチャを基本に考えるとよいです。

外側を変更したいなら参照キャプチャを使う

ラムダ内から外側の変数を変更したい場合は、参照キャプチャを使います。

ただし、そのラムダが実行される時点で、参照先の変数が確実に生きている必要があります。

短いスコープ内で、その場ですぐにラムダを実行する場合は、参照キャプチャでも問題になりにくいです。

一方、ラムダを保存したり、別スレッドで実行したりする場合には、参照キャプチャは慎重に扱うべきです。

大きなオブジェクトはコピーコストに注意する

値キャプチャは安全ですが、大きなオブジェクトをコピーするとコストが高くなることがあります。

その場合は、参照キャプチャ、ポインタ、スマートポインタ、ムーブキャプチャなどを検討します。

ただし、コピーコストを避けるために参照キャプチャを使う場合でも、寿命管理を誤ると危険です。

安全性と性能のバランスを考える必要があります。

実務でのベストプラクティス

キャプチャ対象を明示する

実務では、キャプチャ対象を明示した方が読みやすく、安全です。

デフォルトキャプチャは便利ですが、ラムダが大きくなると、どの変数をキャプチャしているのか分かりにくくなります。

特に、参照キャプチャをデフォルトで使うと、意図しない変数まで参照してしまう可能性があります。

重要な処理では、どの変数を値で持ち、どの変数を参照するのかを明示するとよいです。

返すラムダでは参照キャプチャを避ける

関数からラムダを返す場合、ローカル変数を参照キャプチャするのは避けるべきです。

関数が終了すると、そのローカル変数は破棄されます。

その後にラムダを呼び出すと、破棄済みの変数を参照することになります。

このような場合は、値キャプチャを使うのが基本です。

非同期処理では参照キャプチャを慎重に使う

スレッドや非同期処理では、ラムダがいつ実行されるか分からないことがあります。

そのため、参照キャプチャは非常に慎重に扱う必要があります。

値キャプチャ、スマートポインタ、ムーブキャプチャなどを使って、必要なデータの寿命を明確に管理する方が安全です。

thisの寿命に注意する

メンバ関数内のラムダで this をキャプチャする場合、元のオブジェクトの寿命に注意が必要です。

ラムダがオブジェクトより長く生きる可能性があるなら、this キャプチャは危険です。

状況に応じて、*this によるコピー、shared_ptrweak_ptr などを検討するとよいです。

前回内容の正確性について

大筋では正しい

前回の説明は、C++のラムダ式におけるキャプチャの入門〜中級向け解説として、大筋では正しい内容です。

値キャプチャ、参照キャプチャ、デフォルトキャプチャ、混在キャプチャ、mutable、初期化キャプチャ、this*this、非同期処理での注意点など、主要なトピックは適切に扱えています。

実務上重要な「参照キャプチャの寿命問題」についても触れており、全体として信頼できる説明です。

修正した方がよい点

一方で、より厳密にするなら、いくつか修正した方がよい表現があります。

特に重要なのは、ループ変数を参照キャプチャした例です。

典型的に同じ値が出る、という説明だけでは不十分で、ループ終了後に呼び出す場合は未定義動作になり得ると明確に説明した方が正確です。

また、値キャプチャした変数が変更できない理由については、「変数がconstのようになる」というより、「ラムダの呼び出し演算子がデフォルトでconstになる」と説明した方が正確です。

さらに、デフォルト値キャプチャについても、「使う変数をすべて必ずコピーする」と断定するより、「必要になった外側のローカル変数を値キャプチャできる指定」と表現した方が厳密です。

メンバ変数については、デフォルト値キャプチャを使っても、メンバ変数そのものがコピーされるわけではなく、基本的には this 経由のアクセスになる点も重要です。

まとめ

キャプチャの基本

C++のラムダ式におけるキャプチャは、外側のローカル変数をラムダ内で使うための仕組みです。

値キャプチャでは、ラムダ作成時点の値をコピーして保持します。

参照キャプチャでは、外側の変数そのものを参照します。

値キャプチャは安全性が高く、参照キャプチャは外側の変数を変更できる反面、寿命管理に注意が必要です。

実務で意識すべきこと

実務では、迷ったら値キャプチャを基本に考えると安全です。

外側の変数を変更したい場合や、コピーコストを避けたい場合には参照キャプチャを使います。

ただし、その場合は参照先がラムダ実行時点まで確実に生きていることを確認する必要があります。

ラムダを保存する、返す、非同期処理に渡す、コールバックとして登録する、といった場面では、参照キャプチャは特に注意が必要です。

また、メンバ関数内でラムダを使う場合は、this の寿命にも注意しなければなりません。

安全で読みやすいコードを書くためには、デフォルトキャプチャに頼りすぎず、キャプチャする変数を明示することが大切です。

以上、C++のラムダ式のキャプチャについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次