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

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

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


person David Stone    schedule 05.06.2012    source источник


Ответы (5)


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

В прошлом году я выступал на C ++ Now по теме Выведение типов в C ++ 14 . Я начинаю говорить о push_back vs. 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 будет вызывать только неявные конструкторы. Предполагается, что неявные конструкторы безопасны. Если вы можете неявно построить U из 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. Вместо этого я замаскировал ошибку и обнаружил ее только через несколько месяцев.

person David Stone    schedule 28.04.2016
comment
Было бы полезно, если бы вы могли подробнее рассказать о том, что именно emplace делает в вашем примере, и почему это неправильно. - person eddi; 10.08.2016
comment
@eddi: Я добавил раздел, объясняющий это: std::unique_ptr<T> имеет явный конструктор из T *. Поскольку emplace_back может вызывать явные конструкторы, передача указателя, не являющегося владельцем, компилируется нормально. Однако, когда v выходит за пределы области видимости, деструктор попытается вызвать delete для этого указателя, который не был выделен new, потому что это просто объект стека. Это приводит к неопределенному поведению. - person David Stone; 10.08.2016
comment
Спасибо, что разместили это. Я не знал об этом, когда писал свой ответ, но теперь мне жаль, что я не написал его сам, когда я узнал его позже :) Я действительно хочу дать пощечину людям, которые переключаются на новые функции, просто чтобы сделать самое модное, что они могут найти . Ребята, люди использовали C ++ и до C ++ 11, и не все в этом было проблематично. Если вы не знаете, почему вы используете функцию, не используйте ее. Я так рад, что вы опубликовали это, и я надеюсь, что он получит больше голосов и превзойдет мой. +1 - person user541686; 19.08.2017
comment
@DavidStone Вы хотели сказать «неявный» вместо «явный» в своем ответе? - person Captain Jack sparrow; 31.10.2017
comment
@CaptainJacksparrow: Похоже, я говорю неявно и явно там, где я имею в виду их. Какую часть вы запутали? - person David Stone; 01.11.2017
comment
@DavidStone Разве это утверждение: std::unique_ptr<T> имеет явный конструктор из T * означает, что программист действительно написал этот конструктор. Но я подумал, что вы хотите подразумевать конструктор, который автоматически предоставляется языком или генерируется компилятором (неявно). - person Captain Jack sparrow; 01.11.2017
comment
@CaptainJacksparrow: explicit конструктор - это конструктор, к которому применено ключевое слово explicit. Неявный конструктор - это любой конструктор, не имеющий этого ключевого слова. В случае конструктора std::unique_ptr из T *, разработчик std::unique_ptr написал этот конструктор, но проблема здесь в том, что пользователь этого типа вызвал emplace_back, который вызвал этот явный конструктор. Если бы это было push_back, вместо вызова этого конструктора он использовал бы неявное преобразование, которое может вызывать только неявные конструкторы. - person David Stone; 02.11.2017
comment
Отличный ответ. Как бы то ни было, Совет недели № 112 от Abseil утверждает то же самое. - person Konrad Rudolph; 04.02.2021

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

struct aggregate {
    int foo;
    int bar;
};

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

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

person Luc Danton    schedule 05.06.2012
comment
Обратите внимание, что это применимо только к агрегированной инициализации и инициализации списка инициализаторов. Если вы будете использовать синтаксис {} для вызова реального конструктора, вы можете просто удалить {} и использовать emplace_back. - person Nicol Bolas; 05.06.2012
comment
Время глупых вопросов: значит, emplace_back вообще нельзя использовать для векторов структур? Или просто не для этого стиля с использованием буквального {42,121}? - person Phil H; 05.06.2012
comment
@LucDanton: Как я уже сказал, это применимо только к инициализации aggregate и initializer-list. Вы можете использовать синтаксис {} для вызова реальных конструкторов. Вы можете задать aggregate конструктор, который принимает 2 целых числа, и этот конструктор будет вызываться при использовании синтаксиса {}. Дело в том, что если вы пытаетесь вызвать конструктор, emplace_back будет предпочтительнее, поскольку он вызывает конструктор на месте. И поэтому не требует, чтобы тип был копируемым. - person Nicol Bolas; 05.06.2012
comment
Почему std::initializer_list<int>? Не думаю, что emplace_back({ anything here }) когда-нибудь сработает. - person Johannes Schaub - litb; 06.06.2012
comment
@NicolBolas А, меня смутило, что вы используете только this: к чему относится this? (Точно так же то, что в нем применяется?) В push_back я ценю не то, что возможно использовать единообразную инициализацию для некоторых случаев, а то, что это надежно, например, тип значения является агрегированным или нет, как вы упомянули. - person Luc Danton; 06.06.2012
comment
Исправлена ​​некоторая путаница, связанная с передачей { a, b, c } в шаблоны функций. - person Luc Danton; 06.06.2012
comment
@NicolBolas: Я не думаю, что push_back требует, чтобы тип был копируемым в C ++ 11, не так ли? - person user541686; 29.06.2013
comment
std :: tuple позволит использовать emplace_back в этом случае - person paulm; 29.07.2014
comment
@NicolBolas Значит, единственная причина использовать push_back - это агрегированная инициализация и инициализация списка инициализаторов? Поскольку это не совсем то же самое, что и унифицированная инициализация, не следует ли изменить этот ответ? - person Jon; 31.07.2014
comment
Это было воспринято как дефект в стандарте и было решено. См. cplusplus.github.io/LWG/lwg-active.html#2089 - person David Stone; 28.04.2016
comment
@DavidStone Если бы проблема была решена, ее бы не было в активном списке ... нет? Кажется, это остается нерешенной проблемой. В последнем обновлении, озаглавленном [2018-08-23 Batavia Issues processing], говорится, что P0960 (в настоящее время в разработке) должен решить эту проблему. И я все еще не могу скомпилировать код, который пытается emplace агрегировать без явное написание шаблонного конструктора. На данный момент также неясно, будет ли это рассматриваться как дефект и, следовательно, иметь право на резервное копирование, или пользователи C ++ ‹20 останутся SoL. - person underscore_d; 06.01.2019
comment
@underscore_d: Отличный момент. Я неправильно понял статус проблемы, когда разместил этот комментарий. Вместо этого в C ++ 20 эта проблема решена с помощью open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0960r3.html, который был признан стандартом. - person David Stone; 28.08.2020

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

person user541686    schedule 05.06.2012
comment
Похоже, это проклятие C ++. С каждым новым выпуском мы получаем массу интересных функций, но многие компании либо застревают на использовании какой-либо старой версии ради совместимости, либо не поощряют (если не запрещают) использование определенных функций. - person Dan Albert; 06.07.2013
comment
@DanAlbert: Не уверен, почему это проклятие. Зачем использовать новую функцию, если достаточно старой? - person user541686; 12.11.2013
comment
@Mehrdad: Зачем довольствоваться достаточным, если можно добиться большего? Я бы точно не хотел программировать в blub, даже если бы этого было достаточно. Не говоря уже о том, что это относится к этому примеру, но для человека, который тратит большую часть своего времени на программирование на C89 ради совместимости, это определенно реальная проблема. - person Dan Albert; 14.11.2013
comment
@DanAlbert: Я не делал общих заявлений, я просто говорил о push_back lol. - person user541686; 14.11.2013
comment
Я не думаю, что это действительно ответ на вопрос. На мой взгляд, он просит варианты использования, в которых push_back предпочтительнее. - person Mr. Boy; 03.11.2015
comment
@ Mr.Boy: Это предпочтительнее, если вы хотите иметь обратную совместимость с компиляторами до C ++ 11. Было ли это непонятным в моем ответе? - person user541686; 18.07.2016
comment
Это привлекло гораздо больше внимания, чем я ожидал, поэтому для всех, кто это читает: emplace_back не отличная версия push_back. Это потенциально опасная версия. Прочтите другие ответы. - person user541686; 19.08.2017
comment
@ user541686, вы должны добавить этот последний свой комментарий к самому ответу, так как он важен. Кроме того, есть много других ответов, поэтому просто сказать прочтите другие ответы недостаточно. Пожалуйста, процитируйте пару других ответов, которые вы считаете наиболее подходящими, и дайте ссылку на них. - person Gabriel Staples; 11.11.2020

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

Чтобы учесть известные ошибки компилятора, предпочитайте usingstd::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() привел к серьезному сбою.

person Marc    schedule 10.02.2015
comment
Ждать. Похоже, это ошибка. Как push_back в этом случае может быть иначе? - person balki; 04.03.2015
comment
Вызов emplace_back () использует идеальную пересылку для выполнения построения на месте, и поэтому v [0] не оценивается до тех пор, пока размер вектора не будет изменен (в этой точке v [0] недействителен). push_back создает новый элемент и копирует / перемещает элемент по мере необходимости, а v [0] оценивается до любого перераспределения. - person Marc; 06.03.2015
comment
Вы можете привести пример компилятора, в котором вы видите такое поведение? Я не могу воспроизвести эту проблему. Я подозреваю, что вы на самом деле видели ссылку на v [0], которая становится недействительной при перераспределении вектора. - person cdyson37; 31.03.2015
comment
@ cdyson37 Описанное поведение наблюдалось при использовании компилятора Visual Studio 2012 версии 11, обновление 4. - person Marc; 07.04.2015
comment
@David - Хотя новое пространство должно существовать до того, как старое будет уничтожено, я не думаю, что есть какие-либо гарантии относительно того, когда оценивается параметр emplace_back. Идеальная пересылка позволяет отложить оценку. По моим наблюдениям, старые векторные итераторы становятся недействительными до того, как параметр будет оценен в компиляции, на которой я тестировал, и детали в значительной степени зависят от реализации. - person Marc; 06.08.2015
comment
@Marc: Стандарт гарантирует, что emplace_back работает даже для элементов внутри диапазона. - person David Stone; 07.08.2015
comment
@Marc, почему v [0] не оценивается до тех пор, пока размер вектора не будет изменен? не могли бы вы описать это подробно? - person camino; 10.12.2015
comment
@DavidStone: Не могли бы вы указать, где в стандарте гарантируется такое поведение? В любом случае Visual Studio 2012 и 2015 демонстрируют некорректное поведение. - person Marc; 12.12.2015
comment
@cameino: emplace_back существует, чтобы отложить оценку своего параметра, чтобы уменьшить ненужное копирование. Поведение либо не определено, либо ошибка компилятора (ожидается анализ стандарта). Недавно я провел тот же тест с Visual Studio 2015 и получил 123,3 в разделе Release x64, 123,40 в разделе Release Win32 и 123, -572662307 в разделе Debug x64 и Debug Win32. - person Marc; 12.12.2015
comment
Стандартная гарантия находится на странице eel.is/c++. draft / container.requirements # tab: container.seq.opt. Гарантия конкретно не указана как таковая, а скорее, когда стандарт описывает поведение, он не включает предварительное условие о том, что объект еще не находится в контейнере, поэтому для выполнения этой работы требуются реализации. К сожалению, в Visual Studio есть ошибка. :( - person David Stone; 28.08.2020

Рассмотрим, что происходит в Visual Studio 2019 с компилятором c ++ - 17. У нас есть emplace_back в функции с правильно настроенными аргументами. Затем кто-то меняет параметры конструктора, вызываемого emplace_back. В VS нет никаких предупреждений, код также компилируется нормально, а затем вылетает во время выполнения. После этого я удалил все emplace_back из кодовой базы.

person Radek    schedule 27.08.2020
comment
Я не понимаю, в чем была ваша проблема. - person David Stone; 28.08.2020