読者です 読者をやめる 読者になる 読者になる

ムラサメ研究ブログ

主にゲームやプログラミングのログ

クラスのコンストラクタを可変長テンプレートにして ごにょごにょ

前回なやんだ続きではあります

関数テンプレートは暗黙的インスタンス化可能だが、クラステンプレートは不可能 ならば コンストラクタを可変長テンプレートすればいいじゃない

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

#include<iostream>
#include<sstream>

using namespace std;

struct Hoge{
void stats_(){};
stringstream ss_;

template <class Head, class... Tail>
void stats_(Head&& head, Tail&&... tail){
    ss_ << head;  
    stats_(std::forward<Tail>(tail)...);
}

template <class... T>
    Hoge (T&&... t){
        stats_(std::forward<T>(t)...);
}

friend ostream& operator << (ostream& os, const Hoge& hoge);
    
};
ostream& operator << (ostream& os, const Hoge& hoge){
    os << hoge.ss_.str();
    return os;
}


int main()
{
    cout << Hoge("aaa,", "bbb,", "ccc,") << endl;
}

つまり 関数オブジェクトは

#include<iostream>
#include<sstream>

using namespace std;

struct Hoge{
void stats_(){};
stringstream ss_;

template <class Head, class... Tail>
void stats_(Head&& head, Tail&&... tail){
//    std::cout << head;
    ss_ << head;  
    stats_(std::forward<Tail>(tail)...);
}

template <class... T>
    Hoge& operator() (T&&... t){
        stats_(std::forward<T>(t)...);
    return *this;
}

friend ostream& operator << (ostream& os, const Hoge& hoge);
    
};
ostream& operator << (ostream& os, const Hoge& hoge){
    os << hoge.ss_.str();
    return os;
}


int main()
{
    cout << Hoge()("aaa,", "bbb,", "ccc,") << endl;
}

こうやればできたんだなーー

関数オブジェクトでは 自分を返して汎用的にしてみたけど、何を返すのが正しいのかは わからない

関数テンプレートは暗黙的インスタンス化できる

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

で終わりなんだけど、少し悩んでやっぱ無理そうだったのでメモ 関数テンプレートで実装出来るのだが、使い勝手や最適化など考えて 関数オブジェクトにしたかったが 関数とクラスの テンプレートの違いで出来なかった

追記:やっぱできたぜ。メンバをテンプレートにすればOK

クラスのコンストラクタを可変長テンプレートにして ごにょごにょ - ムラサメ研究ブログ

テンプレート関数は暗黙的インスタンス化可能です!

template<T>
void hoge(T t){};


hoge(12);

可能というかむしろ、暗黙的に使うよね。

もちろん あえて明示的にも書ける

template<T>
void hoge(T t){};


hoge(12);

クラスは暗黙的インスタンス化できない

#include<iostream>


template <class T>
struct Stats{
    void operator()(T t){} const;
};

int main()
{
    Stats<int>()( 12 );
//    Stats()( 12 );   // 暗黙的インスタンス化できない
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

右辺値参照~完全転送まで100%理解するぞ! part6 Universal Reference

やっと UniversalReferenceですよ!

その前に復習

右辺値参照を引数にする時は あまり気にせず下記のようにするのが正しいのでしたね

#include <iostream>

using namespace std;

class X{
public:
    // explicit付けられない!
    X(X& x){
        cout << "copy" << endl;
    }
    X(X&& x){
        cout << "move" << endl;
    }
};

void hoge(X x){
    cout << "hoge(X)" << endl;
}

int main()
{
    X x;
    hoge(x);
    hoge(move(x));
}

呼ぶときに std::move() しなければコピーコンストラクタ、つければムーブコンストラクタが働く これは 暗黙的変換を利用し XとX&& を受け入れています

でも、コピーせず左辺値参照を渡したい事多いよね? コピーするだけ無駄だし それに STLのなかには explicit付きのものしか呼べない関数もあったりで 明示的に行いたい

そんな時は愚直に 左辺値参照と右辺値参照の関数を2つ(const参照いれて3個)実装すればよい

#include <iostream>

using namespace std;

class X{
public:
    // explicit付けられる。付けれるなら絶対付けた方がよい
    explicit X(X& x){
        cout << "copy" << endl;
    }
    explicit X(const X& x){
        cout << "const copy" << endl;
    }
    explicit X(X&& x){
        cout << "move" << endl;
    }
};

void hoge(X& x){
    cout << "hoge(X&)" << endl;
}
void hoge(const X& x){
    cout << "hoge(const X&)" << endl;
}
void hoge(X&& x){
    cout << "hoge(X&&)" << endl;
}


int main()
{
    const X x;

    hoge( x );
    hoge( std::move(x) );
}

このようにすれば、当然 右辺値の時と左辺値の時で処理をわけられる

しかし、毎回同じような関数が3個。引数が増えればさらに倍増していく

それを防ぐのが Universal Referenceである

Universal Reference

#include <iostream>

using namespace std;

class X{
public:
    X(){
        cout << "constuct" << endl;
    }
    explicit X(X& x){
        cout << "copy" << endl;
    }
    explicit X(const X& x){
        cout << "const copy" << endl;
    }
    explicit X(X&& x){
        cout << "move" << endl;
    }
    X& operator =(X& x){
        if(&x==this) return(*this);
        cout << "copy assignment" << endl;
        return(*this);
    }
    X& operator =(const X& x){
        if(&x==this) return(*this);
        cout << "const copy asignment" << endl;
        return(*this);
    }
    X&operator =(const X&& x){
        if(&x==this) return(*this);
        cout << "move asignment" << endl;
        return(*this);
    }
    ~X(){
        cout << "destuct" << endl;
    }
    
    void foo(){};
};

class Hoge{
public:
    template<class T>
    void hoge(T&& x){
        // x_ = x  if lvalue
        // x_ = std::move(x) if rvale
        cout << "hoge(T&&)" << endl;
    }

private:
    X x_;
};


int main()
{
    Hoge h;
    cout << "____" << endl;
    {
        X x;
        h.hoge( x );
    }
    cout << "____" << endl;
    {
        const X x;
        h.hoge( x );
    }
    cout << "____" << endl;
    {
        X x;
        h.hoge( std::move(x) );
    }
    cout << "____" << endl;
    {
        h.hoge( X() );
    }
    cout << "____" << endl;
    {
        h.hoge( std::move(X()) );
    }
    cout << "____" << endl;
}




constuct
____
constuct
hoge(T&%)
destuct
____
constuct
hoge(T&%)
destuct
____
constuct
hoge(T&%)
destuct
____
constuct
hoge(T&%)
destuct
____
constuct
hoge(T&%)
destuct
____
destuct

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

template を使うと、 左辺値: T=X& も 右辺値: T=X&& もどちらも受け取れる

そして 関数テンプレート void hoge(T&& x) に Tが X&の時とX&&の時を考える

ここは 前回の 参照の圧縮を使えば TがX&の時は hoge(X& x) 

TがX&&の時は hoge(X&& x) となる

ところが問題はコメント部分である 引数 xが 左辺値(X&) の時は copy assignment(X& operator =(X&)) を行いたいが 右辺値(X&&)の時は move assignment(X& operator =(X&&)) を行いたい

そんな時に使えるのが std::forward である。 中身は省略し std::forwardとは、moveと同じく 単なるキャストである std::moveが 右辺値にキャストする事に対し std::forward は、Tの型にキャストを行う

実際に使ってみる

#include <iostream>

using namespace std;

class X{
public:
    X(){
        cout << "constuct" << endl;
    }
    explicit X(X& x){
        cout << "copy" << endl;
    }
    explicit X(const X& x){
        cout << "const copy" << endl;
    }
    explicit X(X&& x){
        cout << "move" << endl;
    }
    X& operator =(X& x){
        if(&x==this) return(*this);
        cout << "copy assignment" << endl;
        return(*this);
    }
    X& operator =(const X& x){
        if(&x==this) return(*this);
        cout << "const copy asignment" << endl;
        return(*this);
    }
    X&operator =(const X&& x){
        if(&x==this) return(*this);
        cout << "move asignment" << endl;
        return(*this);
    }
    ~X(){
        cout << "destuct" << endl;
    }
    
    void foo(){};
};

class Hoge{
public:
    template<class T>
    void hoge(T&& x){
        x_ = std::forward<T>(x);
        cout << "hoge(T&&)" << endl;
    }

private:
    X x_;
};


int main()
{
    Hoge h;
    cout << "____" << endl;
    {
        X x;
        h.hoge( x );
    }
    cout << "____" << endl;
    {
        const X x;
        h.hoge( x );
    }
    cout << "____" << endl;
    {
        X x;
        h.hoge( std::move(x) );
    }
    cout << "____" << endl;
    {
        h.hoge( X() );
    }
    cout << "____" << endl;
    {
        h.hoge( std::move(X()) );
    }
    cout << "____" << endl;
}



constuct
____
constuct
copy assignment
hoge(T&&)
destuct
____
constuct
const copy asignment
hoge(T&&)
destuct
____
constuct
move asignment
hoge(T&&)
destuct
____
constuct
move asignment
hoge(T&&)
destuct
____
constuct
move asignment
hoge(T&&)
destuct
____
destuct

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

std::forwardを使えば、T&、const T&、T&& と3つの引数のオーバーロードを1関数で処理できる

とてもすばらしい。

右辺値参照~完全転送まで100%理解するぞ! part5 参照の圧縮(reference collapsing)

右辺値参照の山場の一つ Universal Referenceまであと一歩

参照の圧縮とは

UniversalReferenceそのものだが

参照は(const等のストレージクラス除き)2種類しかない

X& (左辺値参照)、X&&(右辺値参照)

X&&& 等はエラーですし X& && 等、参照を重ねる事は出来ない

でも、templateを使うと不運にも参照が重なる事がある

template<class T>
void hoge(T& t){
}

例えばこのようなテンプレートに X&型の変数を適用したら T&は X& & になり、参照が重なってしまう これらの参照が重なった時はどうなるのだろうか?

コンパイルエラーにすると、テンプレートに参照を渡すものがほとんど動かなくなる C++では当然 参照が重なった場合に明確な法則がある

#include <iostream>

using namespace std;

class X{
public:
    X(){
    }
     X(X& x){
        cout << "copy" << endl;
    }
     X(X&& x){
        cout << "move" << endl;
    }
};

int main()
{
    using Y=X&;
    using Z=X&&;

    X x;

    X( (Y&)x );
    X( (Y&&)x );
    X( (Z&)x );
    X( (Z&&)x );

}


----

copy
copy
copy
move

template 以外に typedef (using) でも参照を重ねる事が出来るので 試してみた

結果は X& & -> X&

X& && -> X&

X&& & -> X&

X&& && -> X&&

と、結論からいえば すべて右辺値参照の時のみ右辺値参照。それ以外は左辺値参照となる

このことを 参照の圧縮(reference collapsing) と呼ぶ

右辺値参照~完全転送まで100%理解するぞ! part4 Universal Reference その前に

こうやって 人に見せようと書く事は自分の理解になる

復習DEATH

前回の復習。右辺値参照を関数の引数にするには 一見コピーに見えるが void hoge( X x ) とプロトタイプすればよい 呼び出し側で hoge( std::move(x) ) とすればムーブコンストラクタが使われる という事 で 復習

#include <iostream>

using namespace std;

class X{
public:
    X(){
        cout << "constuct" << endl;
    }
    explicit X(X& x){
        cout << "copy" << endl;
    }
    explicit X(const X& x){
        cout << "const copy" << endl;
    }
    explicit X(X&& x){
        cout << "move" << endl;
    }
    ~X(){
        cout << "destuct" << endl;
    }
};

void hoge(X x){
    cout << "hoge(X)" << endl;
}

int main()
{
    {
        X x;
        hoge( x );
    }
    cout << "____" << endl;
    {
        const X x;
        hoge( x );
    }
    cout << "____" << endl;
    {
        X x;
        hoge( std::move(x) );
    }
    cout << "____" << endl;
    {
        hoge( X() );
    }
    cout << "____" << endl;
    {
        hoge( std::move(X()) );
    }
}



constuct
copy
hoge(X)
destuct
destuct
____
constuct
const copy
hoge(X)
destuct
destuct
____
constuct
move
hoge(X)
destuct
destuct
____
constuct
hoge(X)
destuct
____
constuct
move
hoge(X)
destuct
destuct

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

std::move() で呼ぶとムーブされ、何もつけないとコピーされる。正しい使い方だ。 少し気になるのは X() と右辺値で直接呼んだ場合は一時オブジェクトが生成されず効率的だが std::move( X() ) とすると、一時オブジェクトを生成し、それを右辺値にキャストしムーブコンストラクタが発生する (最適化で このへんは最適化されると思うけど、実装依存なので 可能なら コードで最適化しておきたい)

本題

上のコードで納得いかないところ オブジェクトの参照だけで良い時にも無駄にオブジェクトのコピーが走る 納得いかないので 調査をする

前回は一緒に受け取るようにした 参照、const参照、右辺値参照に分割してみる (先にこちらから説明すべきであった 反省)

#include <iostream>

using namespace std;

class X{
public:
    X(){
        cout << "constuct" << endl;
    }
    explicit X(X& x){
        cout << "copy" << endl;
    }
    explicit X(const X& x){
        cout << "const copy" << endl;
    }
    explicit X(X&& x){
        cout << "move" << endl;
    }
    X& operator =(X& x){
        if(&x==this) return(*this);
        cout << "copy assignment" << endl;
        return(*this);
    }
    X& operator =(const X& x){
        if(&x==this) return(*this);
        cout << "const copy asignment" << endl;
        return(*this);
    }
    X&operator =(const X&& x){
        if(&x==this) return(*this);
        cout << "move asignment" << endl;
        return(*this);
    }
    ~X(){
        cout << "destuct" << endl;
    }
    
    void foo(){};
};

static X xx;
/*
void hoge(X x){
    xx = x;
    cout << "hoge(X)" << endl;
}
*/
void hoge(X& x){
    xx = x;
    cout << "hoge(X&)" << endl;
}
void hoge(const X& x){
    xx = x;
    cout << "hoge(const X&)" << endl;
}
void hoge(X&& x){
    xx = std::move(x);
    cout << "hoge(X&&)" << endl;
}


int main()
{
    cout << "____" << endl;
    {
        X x;
        hoge( x );
        x.foo();
    }
    cout << "____" << endl;
    {
        const X x;
        hoge( x );
    }
    cout << "____" << endl;
    {
        X x;
        hoge( std::move(x) );
    }
    cout << "____" << endl;
    {
        hoge( X() );
    }
    cout << "____" << endl;
    {
        hoge( std::move(X()) );
    }
    cout << "____" << endl;
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

面倒なので static変数にコピーしているが、普通はクラスのメンバに代入するだろう (explicit付けたよ! 前回の例では つけられないが ムーブコンストラクタにはexplicit付けるべき)

左辺値参照が引数に入ってきたら、中身を自分にコピー 右辺値参照であればムーブ

バッチリ!!

ところがこれを前回のように hoge(X x) で両方を受け取ると困った事になる

xx = x; と書けば ムーブ出来る時でもコピーになるし xx = std::move(x); では、常にムーブになる

ではやはり、 参照、const参照、右辺値参照 と常に3種類の関数が必要なのか??? また 引数がふえれば 6種類、9種類・・・ 組み合わせ爆発してしまう

そこで登場するのが Universal Reference

次回ご期待!

右辺値参照~完全転送まで100%理解するぞ! part3 関数呼び出し

いまいちまだ 納得していない部分もある(理解不足

実践的な関数呼び出し

右辺値参照はムーブセマンティクスのために使われる ムーブセマンティクスは主に ・ 所有権の移動 ・ コピーのコストを削減(ポインタのすげ替え)

で使われる

所有権の移動

コピーされたら困る(インスタンスが複数になってはダメ)ものに 例えば スレッドや、ユニークポインタ等で使う

#include <iostream>
#include <thread>

using namespace std;

int main()
{
  thread thread_;
  
  
  // ムーブコンストラクタが呼ばれるので threadは2個出来ない
  thread_ = thread( [] { cout << "done" << endl; });  
  
  // コピーコンストラクタ不可能
  // thread thread2_ = thread_;   

  if (thread_.joinable()) thread_.join();

}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

上記コードは std::threadは コピーコンストラクタがdeleteされ、ムーブコンストラクタしか無い thread = thread( [] { cout << “done” << endl; });
は、右辺値参照であるため、ムーブコンストラクタが呼ばれ、結果的に threadの所有権が thread
に渡るので 1個しかスレッドが作成されない もし仮に コピーコンストラクタで動いた場合は 一時オブジェクトとしてスレッドが作成され、さらに同じものを thread_にコピーし スレッドは2個作成されるものと思われる

thread thread2 = thread;
は、もちろんコピーコンストラクタは無いため コンパイルエラーになる

同じく unique_ptrもコピーコンストラクタが無く、ムーブコンストラクタのみ許可されているため ポインタが2個になる事はない

コピーのコストを削減

こちらが、よく使う方。

#include <iostream>

using namespace std;

class X{
public:
    X(X& x){
        cout << "copy" << endl;
    }
    X(X&& x){
        cout << "move" << endl;
    }
};


int main()
{
    X x;
    X x1 = x;          // lvalue
    X x2 = move(x);    // rvalue
    X x3 = move(X());  // rvalue
}



copy
move
move

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

復習になるけど、lvalueとrvalueにより コピーとムーブになる

このことを踏まえ 正しい関数呼び出しは

#include <iostream>

using namespace std;

class X{
public:
    X(){
        cout << "constuct" << endl;
    }
    X(X& x){
        cout << "copy" << endl;
    }
    X(X&& x){
        cout << "move" << endl;
    }
    ~X(){
        cout << "destuct" << endl;
    }
};

X hoge(X x){
    return( move(x));
}

int main()
{
    X x;
    hoge(move(x));
}


constuct
move
move
destuct
destuct
destuct

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

上記が正しい関数プロトタイプである

実際には hogeのreturnには std::moveを書かなくとも、moveしてくれるので書かないのが普通である

ただし、見てわかるとおり hogeを呼ぶ際に std::moveを忘れて hoge(x) とすると

constuct copy move destuct destuct destuct

と、当然のように コピーコンストラクタが走るので、使うときによく確認しなければならない

もちろん、コピー禁止にするなら hogeの引数を X&& にすればよい

#include <iostream>

using namespace std;

class X{
public:
    X(){
        cout << "constuct" << endl;
    }
    X(X& x){
        cout << "copy" << endl;
    }
    X(X&& x){
        cout << "move" << endl;
    }
    ~X(){
        cout << "destuct" << endl;
    }
};

X hoge(X&& x){
    return( move(x));
}

int main()
{
    X x;
    hoge(move(x));
}


constuct
move
destuct
destuct

と、さらに処理が速くなり、左辺値の場合はコンパイルエラーになるので、ムーブオンリーにできる

まとめ

大体の場合は、関数プロトタイプでは 実体を渡せばいい X hoge(X x) 関数を呼ぶときに 右辺値であればムーブ、左辺値であればコピーされる コピーしたくない時は、呼ぶときに ムーブセマンティクス忘れないように!

右辺値参照~完全転送まで100%理解するぞ! part2 VisualStudioの罠

はじめに

おまけ。 右辺値参照を理解するうえで大きな妨げになった VisualStudioでの挙動(バグ?? 詳細希望)

左辺値は暗黙で右辺値参照にキャスト出来ない

バグ? の前に基礎から 左辺値(参照)は 左辺値にのみ 右辺値(参照)は 右辺値にのみ 暗黙キャストがゆるされる

#include<iostream>

void foo1(int& i) {}
void foo2(int&& i) {}


auto main() -> int{

    int i;
    int &i1(i);                        // lvalue -> lvalue ref         well-formed
    int &i2(i1);                   // lvalue ref -> lvalue ref     well-formed
// int &i3(1);                     // rvalue -> lvalue ref          ill-formed

    int &&ii(1);                  // rvalue -> rvalue ref         well-formed
// int &&ii2(ii);                  // lvalue ref -> rvalue ref      ill-formed    iiは右辺値を代入しているがそれ自体は左辺値(アドレスが存在する)
    int &&ii3(std::move(ii));      // rvalue ref -> rvalue ref     well-formed


    foo1(i);                        // lvalue -> lvalue ref         well-formed
    foo1(i1);                       // lvalue ref -> lvalue ref     well-formed
// foo1(1);                        // rvalue -> lvalue ref          ill-formed

    foo2(1);                       // rvalue -> rvalue ref         well-formed
// foo2(ii);                       // lvalue ref -> rvalue ref      ill-formed    iiは右辺値を代入しているがそれ自体は左辺値(アドレスが存在する)
    foo2(std::move(ii));            // rvalue ref -> rvalue ref     well-formed
}

上記の通り、右辺値参照と左辺値参照は 暗黙で変換できないので std::moveでキャストする必要がある

VCの不具合??

stringやvector等で試すと不思議なことがおこった

#include<iostream>

void Str(std::string& s) {
}

auto main() -> int{
  Str( std::string("right value"));  // rvalue -> lvalue ref   ill-formだがVisualStudioでは通る・・
}

clanやgccで試したが 当然エラーになるし VisualStudioでも int等で試すとエラーになるが stringやvector等のコンテナだと いずれも上記のコードが通るので 移植性を考える場合は 上記のコードを書かないようにすべきと思われる