std::vectorへのpush_back(T)、push_back(std::move(T))、emplace_back(T)、emplace_back(std::move(T))の違いについて考察

理解が浅いので間違っている可能性がある。

前提として、std::vectorのpush_backとemplace_backの定義が重要となるので、下記に記載する。

void push_back(const T& x); // (1)
void push_back(T&& x);      // (2) C++11

(2)のxは右辺値参照。

template <class... Args>
void emplace_back(Args&&... args);                      // (1) C++11
template <class... Args>
reference emplace_back(Args&&... args);                 // (1) C++17

template <class... Args>
void vector<bool>::emplace_back(Args&&... args);        // (2) C++11
template <class... Args>
reference vector<bool>::emplace_back(Args&&... args);   // (2) C++17

テンプレート関数の仮引数なので、argsはすべてユニヴァーサル参照。

struct Hoge final
{
public:
    // デフォルトコンストラクタ
    Hoge() = default;
    
    // コピーコンストラクタ
    Hoge(const Hoge& other){
        cout << "コピーコンストラクタ" << endl;
        this->a = other.a;
    }
    
    // ムーブコンストラクタ
    Hoge(Hoge&& other){
        cout << "ムーブコンストラクタ" << endl;
        this->a = other.a;
    }
    
    // コピー代入演算子オーバーロード
    Hoge& operator=(const Hoge& obj){
        cout << "コピー演算子オーバーロード" << endl;
        this->a = obj.a;
        return *this;
    }
    
    // ムーブ代入演算子オーバーロード
    Hoge& operator=(Hoge&& obj){
        cout << "ムーブ演算子オーバーロード" << endl;
        this->a = obj.a;
        return *this;
    }
    
    int a;
};

という構造体があったとして、

Hoge a{};
std::vector<Hoge> v{};
v.push_back(a);

まずはpush_back(T)。
実引数として左辺値が渡されているため、「void push_back(const T& x)」が呼び出され、実行すると、「コピーコンストラクタ」と表示される。
上記は内部的に仮引数xのコピーをコピーコンストラクタで生成して、vに要素を追加するということらしい。実引数aと仮引数xの間は参照渡しなので、インスタンス生成は行われない(はず)。
コピーしているので、emplace_back後に、aの内容を修正しても、v内の要素は変化しない。
「コピー演算時オーバーロード」は出力されないので、コピー代入演算子をつかってコピーしているわけではない模様。

Hoge a{};
std::vector<Hoge> v{};
v.push_back(std::move(a));

push_back(std::move(T))。
実引数aがstd::moveで右辺値として渡されているため、「void push_back(T&& x)」が呼び出され、実行すると、「ムーブコンストラクタ」と表示される。
内部的には、一時オブジェクトx(実質的にaと同じ?)をムーブコンストラクタで、ムーブして、vに要素を追加する。

Hoge a{};
std::vector<Hoge> v{};
v.emplace_back(a);

次に、emplace_back(T)。
実行すると、「コピーコンストラクタ」と表示される。
実引数aが左辺値として渡され、仮引数argsがユニヴァーサル参照となる「template reference emplace_back(Args&&… args)」が呼び出される(C++17の場合)。
これは、内部的に実引数aからコピーコンストラクタを呼び出して、直接vに要素を追加するということらしい。
なので、push_back(T)より、一時的なインスタンスからのコピーが省略されるため速いということらしい。
なお、結局コピーしているので、emplace_back後に、aの内容を修正しても、v内の要素は変化しない。

実引数が左辺値なので、型推論で、仮引数argsが左辺値として解釈されるということだろうか?

※いまいちわかっていないので間違っているかも。詳細がわかったら追記。

Hoge a{};
std::vector<Hoge> v{};
v.emplace_back(std::move(a));

最後に、emplace_back(std::move(T))。
実行すると、「ムーブコンストラクタ」と表示される。
実引数aがstd::moveで右辺値として渡され、仮引数argsがユニヴァーサル参照となる「template reference emplace_back(Args&&… args)」が呼び出される(C++17の場合)。
内部的には実引数aからムーブコンストラクタを呼び出して、直接vに要素を追加するということらしい。
実行結果は、emplace_back(T)と変わらず、emplace_back後に、aの内容を修正しても、v内の要素は変化しない。
ただし、ムーブ演算が実行されているのは確実なので、Hogeが内部的にスマートポインタを持っていたとしたら、挙動が変わる気がする。
この辺は継続調査したいところ。

実引数が右辺値なので、型推論で、仮引数argsが右辺値として解釈されるということだろうか?

※いまいちわかっていないので間違っているかも。詳細がわかったら追記。

参考サイト

C++のムーブと完全転送を知る

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です