Зачем мне использовать push_back вместо emplace_back?

232

С ++ 11 векторов имеют новую функцию emplace_back. В отличие от этого push_back, который полагается на оптимизацию компилятора во избежание копирования, emplace_backиспользует идеальную пересылку для отправки аргументов непосредственно в конструктор для создания объекта на месте. Мне кажется, что emplace_backвсе push_backможет сделать, но иногда это будет лучше (но никогда не хуже).

Какую причину я должен использовать push_back?

Дэвид Стоун
источник

Ответы:

162

За последние четыре года я много думал об этом. Я пришел к выводу, что большинство объяснений по отношению к push_backпротив emplace_backпропускают полную картину.

В прошлом году я выступил с докладом на C ++. Теперь о типе дедукции в C ++ 14 . Я начинаю говорить о push_backпротив emplace_backв 13:49, но есть полезная информация, которая предоставляет некоторые подтверждающие доказательства до этого.

Реальное первичное различие связано с неявными и явными конструкторами. Рассмотрим случай, когда у нас есть один аргумент, который мы хотим передать push_backили emplace_back.

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);

После того, как ваш оптимизирующий компилятор справится с этим, между этими двумя утверждениями не будет разницы в терминах сгенерированного кода. Традиционная мудрость заключается в том, что push_backбудет создаваться временный объект, который затем будет перемещен в, vтогда как emplace_backбудет продвигать аргумент вперед и создавать его непосредственно на месте без копий или перемещений. Это может быть правдой, основываясь на коде, написанном в стандартных библиотеках, но это ошибочное предположение, что работа оптимизирующего компилятора заключается в генерации написанного вами кода. Работа оптимизирующего компилятора состоит в том, чтобы на самом деле генерировать код, который вы написали бы, если бы вы были экспертом по оптимизации для конкретной платформы и не заботились о удобстве сопровождения, а просто о производительности.

Фактическая разница между этими двумя утверждениями заключается в том, что более мощные emplace_backвызовут любой тип конструктора, тогда как более осторожные push_backвызовут только неявные конструкторы. Неявные конструкторы должны быть безопасными. Если вы можете неявно построить a Uиз a T, вы говорите, что Uможете хранить всю информацию Tбез потерь. В любой ситуации безопасно пройти, Tи никто не будет возражать, если вы сделаете это Uвместо. Хорошим примером неявного конструктора является преобразование из std::uint32_tв std::uint64_t. Плохой пример неявного преобразования doubleв std::uint8_t.

Мы хотим быть осторожными в нашем программировании. Мы не хотим использовать мощные функции, потому что чем мощнее функция, тем легче случайно сделать что-то неправильное или неожиданное. Если вы намереваетесь вызывать явные конструкторы, тогда вам нужна сила emplace_back. Если вы хотите вызывать только неявные конструкторы, придерживайтесь безопасности push_back.

Пример

std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile

std::unique_ptr<T>имеет явный конструктор из T *. Поскольку emplace_backмогут вызывать явные конструкторы, передача не принадлежащего указателю компилируется просто отлично. Однако, когда vвыходит из области видимости, деструктор пытается вызвать deleteэтот указатель, который не был выделен, newпотому что это просто объект стека. Это приводит к неопределенному поведению.

Это не просто придуманный код. Это была настоящая производственная ошибка, с которой я столкнулся. Код был std::vector<T *>, но он владел содержимым. В рамках перехода на C ++ 11 я правильно изменил T *на, std::unique_ptr<T>чтобы указать, что вектору принадлежит его память. Тем не менее, я основывал эти изменения от моего понимания в 2012 году, в течение которых я думал , «emplace_back делает все push_back может сделать и больше, так почему бы мне когда - либо использовать push_back?», Поэтому я изменил push_backк emplace_back.

Если бы я вместо этого оставил код как более безопасный push_back, я бы сразу обнаружил эту давнюю ошибку, и это было бы расценено как успешное обновление до C ++ 11. Вместо этого я замаскировал ошибку и не нашел ее несколько месяцев спустя.

Дэвид Стоун
источник
Было бы полезно, если бы вы могли уточнить, что именно emplace делает в вашем примере, и почему это неправильно.
Эдди
4
@eddi: я добавил раздел, объясняющий это: std::unique_ptr<T>имеет явный конструктор из T *. Поскольку emplace_backмогут вызывать явные конструкторы, передача не принадлежащего указателю компилируется просто отлично. Однако, когда vвыходит из области видимости, деструктор пытается вызвать deleteэтот указатель, который не был выделен, newпотому что это просто объект стека. Это приводит к неопределенному поведению.
Дэвид Стоун
Спасибо за публикацию этого. Я не знал об этом, когда писал свой ответ, но теперь я хотел бы написать его сам, когда узнаю об этом позже :) Я действительно хочу дать пощечину людям, которые переключаются на новые функции, просто для того, чтобы сделать самую хиппую вещь, которую они могут найти , Ребята, люди использовали C ++ до C ++ 11, и не все было проблематично. Если вы не знаете, почему используете функцию, не используйте ее . Так рад, что вы опубликовали это, и я надеюсь, что это получит гораздо больше голосов, так что это выше моего. +1
user541686
1
@CaptainJacksparrow: похоже, я говорю неявно и явно, где я их имею в виду. Какую часть вы запутали?
Дэвид Стоун
4
@CaptainJacksparrow: explicitконструктор - это конструктор, к которому explicitприменено ключевое слово . «Неявный» конструктор - это любой конструктор, у которого нет этого ключевого слова. В случае std::unique_ptrс конструктором из T *, разработчик std::unique_ptrнаписал этот конструктор, но проблема здесь в том emplace_back, что вызвал пользователь этого типа , который вызвал этот явный конструктор. Если бы это было push_backвместо вызова этого конструктора, он бы полагался на неявное преобразование, которое может вызывать только неявные конструкторы.
Дэвид Стоун
117

push_backвсегда позволяет использовать унифицированную инициализацию, что мне очень нравится. Например:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

С другой стороны, v.emplace_back({ 42, 121 });работать не будет.

Люк Дантон
источник
58
Обратите внимание, что это относится только к агрегатной инициализации и инициализации списка инициализаторов. Если вы используете {}синтаксис для вызова фактического конструктора, вы можете просто удалить {}и использовать emplace_back.
Никол Болас
Время тупого вопроса: поэтому emplace_back вообще не может использоваться для векторов структур? Или просто не для этого стиля с использованием литерала {42,121}?
Фил Х
1
@LucDanton: Как я уже сказал, это относится только к инициализации агрегатного списка и списка инициализаторов. Вы можете использовать {}синтаксис для вызова реальных конструкторов. Вы могли бы дать aggregateконструктор, который принимает 2 целых числа, и этот конструктор будет вызываться при использовании {}синтаксиса. Дело в том, что если вы пытаетесь вызвать конструктор, emplace_backбыло бы предпочтительнее, так как он вызывает конструктор на месте. И поэтому не требует, чтобы тип был копируемым.
Николь Болас
11
Это было расценено как дефект в стандарте, и было решено. См. Cplusplus.github.io/LWG/lwg-active.html#2089
Дэвид Стоун,
3
@DavidStone Если бы он был решен, он все равно не был бы в «активном» списке ... нет? Похоже, остается нерешенной проблемой. В последнем обновлении под заголовком « [2018-08-23 Обработка ошибок в Батавии] » говорится, что « P0960 (в данный момент в полете) должен решить эту проблему ». И я до сих пор не могу скомпилировать код, который пытается emplaceагрегировать без явной записи конструктора шаблонов. , На этом этапе также неясно, будет ли это рассматриваться как дефект и, следовательно, будет ли он пригоден для бэкпорта, или пользователи C ++ <20 останутся SoL.
underscore_d
80

Обратная совместимость с компиляторами до C ++ 11.

user541686
источник
21
Это кажется проклятием C ++. Мы получаем тонны интересных функций с каждым новым выпуском, но многие компании либо застревают, используя какую-то старую версию ради совместимости, либо не поощряют (если не запрещают) использование определенных функций.
Дэн Альберт
6
@Mehrdad: Зачем соглашаться на достаточное, когда вы можете иметь большое? Я уверен, что не хотел бы программировать в клубе , даже если этого было достаточно. Не говоря уже об этом примере, но как человек, который проводит большую часть своего времени за программированием на C89 ради совместимости, это определенно реальная проблема.
Дэн Альберт
3
Я не думаю, что это действительно ответ на вопрос. Для меня он просит варианты использования, где push_backпредпочтительнее.
Мистер Бой
3
@ Mr.Boy: Желательно, чтобы вы были обратно совместимы с компиляторами до C ++ 11. Было ли это неясно в моем ответе?
user541686
6
Это привлекло гораздо больше внимания, чем я ожидал, поэтому для всех вас, читающих это: emplace_backэто не «отличная» версия push_back. Это потенциально опасная версия. Прочитайте другие ответы.
user541686
68

Некоторые реализации библиотеки emplace_back не ведут себя так, как указано в стандарте C ++, включая версию, поставляемую с Visual Studio 2012, 2013 и 2015.

Чтобы учесть известные ошибки компилятора, предпочтите использовать, std::vector::push_back()если параметры ссылаются на итераторы или другие объекты, которые будут недействительными после вызова.

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

В одном компиляторе v содержит значения 123 и 21 вместо ожидаемых 123 и 123. Это связано с тем, что второй вызов emplace_backприводит к изменению размера, при котором точка v[0]становится недействительной.

Работающая реализация приведенного выше кода будет использовать push_back()вместо emplace_back()следующего:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

Примечание: использование вектора целых для демонстрационных целей. Я обнаружил эту проблему с гораздо более сложным классом, который включал динамически размещенные переменные-члены, и вызов emplace_back()привел к серьезному падению.

Марк
источник
Подождите. Это похоже на ошибку. Как может push_backбыть иначе в этом случае?
балки
12
Вызов emplace_back () использует совершенную пересылку для выполнения построения на месте, и поэтому v [0] не оценивается до тех пор, пока вектор не будет изменен (в этом случае точка v [0] недопустима). push_back создает новый элемент и копирует / перемещает элемент по мере необходимости, а v [0] оценивается перед любым перераспределением.
Марк
1
@Marc: Стандарт гарантирует, что emplace_back работает даже для элементов внутри диапазона.
Дэвид Стоун
1
@DavidStone: Не могли бы вы предоставить ссылку, где в стандарте это поведение гарантировано? В любом случае Visual Studio 2012 и 2015 демонстрируют некорректное поведение.
Марк
4
@cameino: emplace_back существует, чтобы задержать оценку своего параметра, чтобы уменьшить ненужное копирование. Поведение либо неопределенное, либо ошибка компилятора (в ожидании анализа стандарта). Недавно я запустил тот же тест для Visual Studio 2015 и получил 123,3 в Выпуске x64, 123,40 в Выпуске Win32 и 123, -572662307 в Debug x64 и Debug Win32.
Марк