こんにちは。田原です。

前回はラムダ式のコピー・キャプチャを解説しました。コピー・キャプチャは安全なので寿命の長いラムダ式でも安心して使えます。ですが当然コピー負荷が掛かります。コピーせずにパラメータを渡して保持するには参照ですね。今回はこの参照型のメンバ変数でキャプチャする参照キャプチャの使い方と注意点を主に解説します。

1.参照キャプチャの書き方

コピー・キャプチャする時は[=]と書きました。そして、参照キャプチャの場合は`[&]’と書きます。
前回のコピー・キャプチャのサンプルを参照キャプチャに変えてみます。(=を&に変えるだけですね。)

#include <iostream>
#include <string>
 
int main()
{
    // ローカル変数
    std::string aString="<local string>";
 
    // ラムダ式
    auto aLabmda=[&]()
    {
        // ここで親関数のローカル変数 aString にアクセスする
        std::cout << "aLabmda : aString=" << aString << "\n";
    };
 
    // ラムダ式の実行
    aLabmda();
}

結果は当然ですが変わりません。

aLabmda : aString=<local string>

ですので、呼び出し側のノーカル変数がキャプチャで定義されるメンバ変数より寿命が長い時は参照キャプチャを使うとコピーの負荷を節約できます。復習も兼ねて前回と同様に関数オブジェクトにしてみます。

    // 関数オブジェクト
    class foo
    {
        std::string& aString;
    public:
        foo(std::string& iString) : aString(iString) { }
        void operator()()
        {
            std::cout << "foo     : aString=" << aString << "\n";
        };    
    } aFoo(aString);

    // 関数オブジェクトの実行
    aFoo();
foo     : aString=<local string>

前回同様サイズを比較してみます。参照型の非staticメンバ変数は参照先のアドレスを保持しますのでポインタと同じサイズになります。

    // サイズを比較してみる
    std::cout << "sizeof(aString)  =" << sizeof(aString) << "\n";
    std::cout << "sizeof(aLabmda)  =" << sizeof(aLabmda) << "\n";
    std::cout << "sizeof(aFoo)     =" << sizeof(aFoo) << "\n";
sizeof(aString)  =32
sizeof(aLabmda)  =8
sizeof(aFoo)     =8

Wandboxで確認する。

2.参照キャプチャのリスク例

2-1.比較的よく知られている参照の間違った使い方の例

皆さんも御存知のように、ローカル変数を参照型で返却すると呼び出し側が受け取った時には開放されている変数を参照してますから未定義動作を引き起こします。まずはその例です。

#include <iostream>
#include <string>

std::string& getString()
{
    std::string aString="<local string>";
    return aString;
}

int main()
{
    std::cout << "getString()=" << getString() << "\n";
}

警告は出ますが、C++の文法に違反していませんのでコンパイル・エラーにはなりません。
gccでは Segmentation fault(不正メモリアクセス)で落ちました。
clangは落ちませんが変な文字列が出力されました。

Wandboxで確認する。

2-2.参照キャプチャの間違った使い方の例

ラムダ式の寿命がそれを定義した関数の寿命より長い時に参照キャプチャを使うと2-1で示したのと同様な未定義動作を引き起こします。
その例として、比較的判りやすいローカル変数をキャプチャしたラムダ式を返却してみます。

#include <iostream>
#include <string>

auto getPrint()
{
    std::string aString="<local string>";
    return [&](){std::cout << "getPrint() : aString=" << aString << "\n";};
}

int main()
{
    auto aPrint=getPrint();
    std::cout << "--- execute print()\n";
    aPrint();
}

戻されたラムダ式で、getPrint()関数内のローカル変数aStringをアクセスして表示しています。
当然ですが、このラムダ式を実行する時には既にaStringが破棄されていますので、未定義動作を引き起こします。

定義されていない動作ですので何が起こるかわかりません。処理系により結果は異なります。Wandboxのgccでは下記のように変な文字列が表示されました。

--- execute print()
getPrint() : aString=��t��@ ��t�0��t�t@���t��@0X���?��t�H@��9�4]�_�@��t���O��P���I$@L�H@@@�@��t��@��t�6�t�A�t�R�t�d�t���t���t���t���t�!��t���d@@80(�	�@��
data: �����t���t����t��E�3Q4��Q�.�/C�x86_64

Wandboxで確認する。

大事なことなので2回
残念ながら、ローカル変数の参照を返却する時のようには警告してくれませんので、ご注意下さい。
残念ながら、ローカル変数の参照を返却する時のようには警告してくれませんので、ご注意下さい。

3.コピー・キャプチャと参照キャプチャを両方使いたい時

あまり頻繁にはないと思いますが、一部のローカル変数はコピー・キャプチャ、また別のローカル変数は参照キャプチャすることができます。
コピー・キャプチャしたい変数名を[]の中に書き、参照キャプチャしたい変数名に & を付けて[]の中に書きます。

#include <iostream>
 
int main()
{
    // ローカル変数
    int aCopy=0;
    int aRef=0;
 
    // アドレス・チェック
    std::cout << "&aCopy=" << &aCopy << "\n";
    std::cout << "&aRef =" << &aRef << "\n";

    // ラムダ式
    auto aLabmda=[aCopy, &aRef]()
    {
        std::cout << "\n--- aLabmda ---\n";
        std::cout << "&aCopy=" << &aCopy << "\n";
        std::cout << "&aRef =" << &aRef << "\n";
    };
 
    // ラムダ式の実行
    aLabmda();
}

キャプチャしたデータのアドレスを表示してみました。コピー・キャプチャは元のローカル変数とは別ものですからアドレスが異なります。参照キャプチャは元のローカル変数を参照していますから、同じアドレスになります。

&aCopy=0x7ffccf8b60bc
&aRef =0x7ffccf8b60b8

--- aLabmda ---
&aCopy=0x7ffccf8b60a0
&aRef =0x7ffccf8b60b8

また、今までの'[=]’や[&]による指定は、デフォルトのキャプチャを指定します。そして、デフォルトのキャプチャとそうででないキャプチャを指定することも可能です。
ですので、14行目を次のように変更しても同じです。

    auto aLabmda=[=, &aRef]()
    auto aLabmda=[&, aCopy]()

Wandboxで確認する。

4.constとmutable

前回ちょっと触れましたが、コピー・キャプチャされたメンバ変数は const 修飾されています。
コピーされていますから、変更しても元の変数は変更されません。ですので、const修飾することで間違って変更するコードをコンパイル・エラーとし、バグ検出するようになっています。
例えば、次のコードの12行目のコメントを外すとコンパイル・エラーになります。

#include <iostream>
 
int main()
{
    // ローカル変数
    int aCopy=0;
    int aRef=0;

    // ラムダ式
    auto aLabmda=[aCopy, &aRef]()
    {
//      aCopy=123;  // assignment of read-only variable 'aCopy'
        aRef =456;
    };
 
    // ラムダ式の実行
    aLabmda();
 
    // アドレス・チェック
    std::cout << "aCopy=" << aCopy << "\n";
    std::cout << "aRef =" << aRef << "\n";
}

しかし、わざわざメモリ領域をラムダ式内部に確保していますから、それをワークエリアとして使うために変更しいたい場合もあります。そのような時はmutableをラムダ式に追加します。ラムダ式の仮引数リストの後ろにmutableと書きます。(この位置は、メンバ関数のthisを const 修飾する際に メンバ関数の後ろに const と書くのと同じ位置にあたります。)

#include <iostream>
 
int main()
{
    // ローカル変数
    int aCopy=0;
    int aRef=0;

    // ラムダ式
    auto aLabmda=[aCopy, &aRef]() mutable
    {
        aCopy=123;
        aRef =456;
    };
 
    // ラムダ式の実行
    aLabmda();
 
    // アドレス・チェック
    std::cout << "aCopy=" << aCopy << "\n";
    std::cout << "aRef =" << aRef << "\n";
}

効果は単にコピー・キャプチャした変数を変更するコードがコンパイル・エラーにならないだけです。コピー・キャプチャですから、元のローカル変数は変更されません。

参照キャプチャは?
ところで、元々参照キャプチャにはconst修飾されていませんので、mutableは参照キャプチャについては何も効果がありません。

5.まとめ

今回は参照キャプチャについて詳しく解説してみました。参照キャプチャはコピー負荷がないので性能的に有利です。ただし、ローカル変数を参照で返却するようなもので危険と背中合わせです。その危険な使い方の例を示しました。他にもスレッドの実行関数として渡す場合も同様です。スレッドの終了が起動元関数より後で終了する場合に危険です。用法・用量を守って正しくお使いください。

さて、来週はジェネリック・ラムダを解説します。ジェネリック・ラムダを使えばstd::tupleのような静的なリストをスマートに処理できます。お楽しみに。