Range-based for loopの要素型(auto、auto&、auto&&)を実験


何々の時に〇〇コンストラクタが呼出しされるが覚えられないので特訓」の続きです。細々と続けています。

今回は、Range-based for loopの要素型について、autoauto&auto&&のそれぞれを指定した場合の挙動の違いを確認してみました。

実験用クラスXの定義

前回同様、コピーコンストラクタ、ムーブコンストラクタがinvokeされたかどうかを確認するデバッグ文を仕込んだ実験用クラスXを使いまわします。

#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wuninitialized"
#include <iostream>
#include <memory>

class X {
    public:
    X() {
        std::cout << this << " : constructor." << std::endl;
    }
    ~X() {
        std::cout << this << " : destructor." << std::endl;
    }
    X(const X& x) {
        std::cout << this << " <-- " << std::addressof(x) << " : copy constructor." << std::endl;
    }
    X(X&& x) {
        std::cout << this << " <-- " << std::addressof(x) << " : move constructor." << std::endl;
    }
    X& operator=(const X& x) {
        std::cout << this << " <-- " << std::addressof(x) << " : copy assignment operator." << std::endl; return *this;
    }
    X& operator=(X&& x) {
        std::cout << this << "  <-- " << std::addressof(x) << " : move assignment operator." << std::endl; return *this;
    }
    void NonConstFunction(){
        std::cout << this << " : non const function is called." << std::endl;
    }
    void ConstFunction() const {
        std::cout << this << " : const function is called." << std::endl;
    }
};

要素型がnon-referenceの場合

#include <iostream>
#include "X.h"

using namespace std;

int main()
{
    X xs[3];

    for(auto x : xs)
        x.NonConstFunction();
}

ループのたびに、xsの各要素がxにコピーされています。すなわち、ループ1周ごとに、コピーコンストラクタがinvoke→デストラクタがinvokeされるようです。

実行結果
0x7ffee1bef9ac : constructor.
0x7ffee1bef9b0 : constructor.
0x7ffee1bef9b4 : constructor.
0x7ffee1bef9a8 <-- 0x7ffee1bef9ac : copy constructor.
0x7ffee1bef9a8 : non const function is called.
0x7ffee1bef9a8 : destructor.
0x7ffee1bef9a8 <-- 0x7ffee1bef9b0 : copy constructor.
0x7ffee1bef9a8 : non const function is called.
0x7ffee1bef9a8 : destructor.
0x7ffee1bef9a8 <-- 0x7ffee1bef9b4 : copy constructor.
0x7ffee1bef9a8 : non const function is called.
0x7ffee1bef9a8 : destructor.
0x7ffee1bef9b4 : destructor.
0x7ffee1bef9b0 : destructor.
0x7ffee1bef9ac : destructor.

要素型がlvalue referenceの場合

#include <iostream>
#include "X.h"

using namespace std;

int main()
{
    X xs[3];

    for(auto& x : xs)
        x.NonConstFunction();
}

ループのたびに配列xsの各要素がxに束縛されますので、コンストラクタはinvokeされない。

実行結果
0x7ffed7fdecf4 : constructor.
0x7ffed7fdecf8 : constructor.
0x7ffed7fdecfc : constructor.
0x7ffed7fdecf4 : non const function is called.
0x7ffed7fdecf8 : non const function is called.
0x7ffed7fdecfc : non const function is called.
0x7ffed7fdecfc : destructor.
0x7ffed7fdecf8 : destructor.
0x7ffed7fdecf4 : destructor.

要素型がrvalue referenceの場合

#include <iostream>
#include "X.h"

using namespace std;

int main()
{
    X xs[3];

    for(auto&& x : xs)
        x.NonConstFunction();
}

lvalue referenceの場合と同様となるようです(perfect forwardingというのかな)。

0x7ffd963a4db4 : constructor.
0x7ffd963a4db8 : constructor.
0x7ffd963a4dbc : constructor.
0x7ffd963a4db4 : non const function is called.
0x7ffd963a4db8 : non const function is called.
0x7ffd963a4dbc : non const function is called.
0x7ffd963a4dbc : destructor.
0x7ffd963a4db8 : destructor.
0x7ffd963a4db4 : destructor.

xvalueの各要素をrvalue referenceで束縛

#include <iostream>
#include <cstdlib>
#include "X.h"

using namespace std;

struct X2 {
    X xs[3];
} x2;

X2 f()
{
    return x2;
}

int main()
{
    for(auto&& x : f().xs)
        x.NonConstFunction();
}

こういうの(xvalueの式が指し示す配列型オブジェクトの各要素をイテレーション)はどうかな?というと、ループ開始前に、f().xsが指し示す配列型一時オブジェクトの全要素のコピーコンストラクタがinvokeされています。

  • ループ開始時の式f().xsの評価により、同式が指し示す一時オブジェクトがコピーコンストラクタにより構築される。
  • f().xsは(式f()がprvalueなのでそのメンバアクセス演算子の式.xsは)xvalueとなる。
  • f().xsが指し示す一時オブジェクトの各要素はxにより束縛される。
実行結果
0x601bf1 : constructor.
0x601bf2 : constructor.
0x601bf3 : constructor.
0x7ffc9966c54d <-- 0x601bf1 : copy constructor.
0x7ffc9966c54e <-- 0x601bf2 : copy constructor.
0x7ffc9966c54f <-- 0x601bf3 : copy constructor.
0x7ffc9966c54d : non const function is called.
0x7ffc9966c54e : non const function is called.
0x7ffc9966c54f : non const function is called.
0x7ffc9966c54f : destructor.
0x7ffc9966c54e : destructor.
0x7ffc9966c54d : destructor.
0x601bf3 : destructor.
0x601bf2 : destructor.
0x601bf1 : destructor.

xvalueの各要素をlvalue referenceで束縛

コンパイルエラーが出るだろうなと思い、試しにやってみると。

#include <iostream>
#include <cstdlib>
#include "X.h"

using namespace std;

struct X2 {
    X xs[3];
} x2;

X2 f()
{
    return  x2;
}

int main()
{

    for(auto& x : f().xs)
        x.NonConstFunction();
}

Wandbox

あれ、xvalueの各要素への束縛なのに、lvalue referenceでも束縛できてしまう?(f().xsの代わりに型std::vector<bool>をもつ式だと、コンパイルエラーが出るんですが)。

実行結果
0x601bf1 : constructor.
0x601bf2 : constructor.
0x601bf3 : constructor.
0x7ffdd740c0bd <-- 0x601bf1 : copy constructor.
0x7ffdd740c0be <-- 0x601bf2 : copy constructor.
0x7ffdd740c0bf <-- 0x601bf3 : copy constructor.
0x7ffdd740c0bd : non const function is called.
0x7ffdd740c0be : non const function is called.
0x7ffdd740c0bf : non const function is called.
0x7ffdd740c0bf : destructor.
0x7ffdd740c0be : destructor.
0x7ffdd740c0bd : destructor.
0x601bf3 : destructor.
0x601bf2 : destructor.
0x601bf1 : destructor.

最後に

コンパイルエラーが出ない、だけでは合法か違法か判断つかないので、ちょっと規格票読んできます。何か分かったら追記します。