【C++】emplace_back/emplaceとは何か【処理効率やpush_backとの違い】

emplace系関数

C++のvector等のコンテナクラスにはemplaceやemplace_back、emplace_front, emplace_hint等のemplace系メンバ関数が存在します。いずれもコンテナに要素を追加するためのメソッドであり、一般的なinsertやpush_backと似たような動作をしますが、使い方や処理効率が大きく異なっています。

関数名概要
push_back受け取った要素をコンテナに追加する
emplace_back要素型のクラスが取りうるコンストラクタ引数の値を元に、要素をコンテナ内で直接構築して追加する
「要素型のクラスが取りうるコンストラクタ引数の値」とは要するに、std::vector<std::string>コンテナクラスの要素型であるstd::stringのコンストラクタが受け取ることのできる引数型const char*などの値のことです。より詳しい話や目的については次項以降に解説します。

目次

emplace系関数の特徴

emplace系の関数は、要素の生成をコンテナ内部で行うという特徴があります。そのため、emplace系の関数は要素を直接受け取るのではなく、要素型が取りうるコンストラクタ引数の値を受け取るようになっています。

std::vector<std::string> v = {};

/* emplace_back */
const char* cstr = "a";
v.emplace_back(cstr); // std::stringのコンストラクタ引数を受け取る

/* push_back */
std::string str = "b";
v.push_back(str);     // 要素を直接受け取る

そして実際のコンストラクタ呼び出しはコンテナ内部で行われます。上記の例ではemplace_back関数内部でstd::string(cstr)相当の呼び出しが行われることになります。

これらのemplace系関数の利用によって、オブジェクトや一時オブジェクトの余計な生成やコピー/ムーブ処理、破棄処理が回避されるという利点が得られます。

具体的な仕組みと違い

vectorクラスを例に、emplace_back関数とpush_back関数の違いを示します。

std::vector<std::string> v = {};

// emplace_back内部で行われるstd::string("a")オブジェクトの生成コストのみが発生
// オブジェクトは生成と同時にコンテナに追加されるため、コピー/ムーブ処理が発生しない
v.emplace_back("a");

// push_backの場合は値コピーまたはムーブ処理が発生する
// 一時オブジェクトstd::string("b")のデストラクタも呼ばれる
v.push_back(std::string("b"));
// const char*型の値を渡しても、std::string型への変換とコピー/ムーブが発生する
v.push_back("c");

// 要素型を直接渡した場合はpush_backと同等の処理になる
v.emplace_back(std::string("d"));

emplace_back

std::vector<std::string>というstd::stringを要素とするコンテナがあったとすると、上記のemplace_back関数の呼び出し処理はstd::stringのコンストラクタ引数であるconst char*型の値を受け取ることになります。そしてemplace_back関数は受け取ったconst char*型の値を元に、std::string型の要素を生成し、自身のコンテナにその要素を追加します。

push_back

対して、push_back関数に要素型のコンストラクタ引数を渡した場合(v.push_back("c");)には、暗黙の型変換、もとい変換コンストラクタが働くため、結局のところstd::stringの一時オブジェクトが生成されて余計な値コピーやムーブ処理が発生してしまいます。

なお、push_backもemplace_backも、要素型の値を渡した場合にはオブジェクトのコピー/ムーブ処理が発生します(具体的にはコピーコンストラクタやムーブコンストラクタが呼ばれてしまいます)。一時オブジェクトを渡した際にも同様にコピーやムーブが行われます。

emplace系関数の呼び出し方法

emplace系の関数には、要素型のクラスのコンストラクタが受け取ることのできる引数と同等の引数を渡すことができます。

std::vector<std::string> v;
v.emplace_back("ab");   // v == {"ab"}
v.emplace_back(2, 'c'); // v == {"ab", "cc"}
v.emplace_back();       // v == {"ab", "cc", ""}
v.emplace(v.begin(), "A"); // v == {"A", "ab", "cc", ""}
v.emplace(v.begin());  // v == {"", "A", "ab", "cc", ""}

// error: no matching member function for call to 'emplace_back'
v.emplace_back({'d', 'e'});
// OK
v.emplace_back(std::initializer_list<char>{'d', 'e'}); // v == {"", "A", "ab", "cc", "", "de"}

要素型のクラスに、複数の引数を受け取るコンストラクタが存在する場合には、emplace系の関数側でも同様に複数の引数を指定することができます。例えば要素型のクラスでstd::string(2, 'c')によるコンストラクタ呼び出しが行えるとすれば、コンテナ側でも同様にv.emplace_back(2, 'c')による呼び出しが行える事になります。

emplace系の関数に空の引数を指定した場合には、要素型のデフォルトコンストラクタ呼び出しの結果が追加されることになります。

emplace系の関数では{'d', 'e'}という形で初期化リストを直接渡すことはできないため、initializer_listを介した明示的な呼び出しが必要となります。

処理効率について

emplace系関数は余計なオブジェクトの生成やコピーを避ける事ができるため、非常に有効な関数であると言えます。最適化を意識しなければ、仕様上はpush_back関数よりもemplace_back関数を使ったほうが処理効率は良くなります。もっともemplace_backに要素を直接渡した場合の処理(v.emplace_back(std::string("b"));)についてはpush_backと同等の処理が行われるため、処理効率は変わりません。

なおv.push_back(std::string("b"));のような、emplace系以外の関数で一時オブジェクトを挿入する処理については、ムーブコンストラクタが働くため、emplace系関数との処理コストの違いはそれほど大きくならないことがほとんどです。

ユーザ定義型を要素型に持つコンテナの処理コストが問題になっている場合は、emplace系関数への移行や、ユーザ定義型に対するムーブコンストラクタの定義を検討してみると良いかもしれません。パフォーマンスの改善が期待できます。

参考: ムーブコンストラクタとは
広告