Чтобы поддерживать семантику перемещения, следует ли выполнять параметры функции unique_ptr, по значению или по rvalue?
Одна из моих функций принимает вектор как параметр и сохраняет его как переменную-член. Я использую константную ссылку на вектор, как описано ниже.
class Test {
public:
void someFunction(const std::vector<string>& items) {
m_items = items;
}
private:
std::vector<string> m_items;
};
Однако иногда items
содержит большое количество строк, поэтому я хотел бы добавить функцию (или заменить ее на новую), которая поддерживает семантику перемещения.
Я думаю о нескольких подходах, но я не уверен, какой из них выбрать.
1) unique_ptr
void someFunction(std::unique_ptr<std::vector<string>> items) {
// Also, make `m_itmes` std::unique_ptr<std::vector<string>>
m_items = std::move(items);
}
2) перейдите по значению и переместите
void someFunction(std::vector<string> items) {
m_items = std::move(items);
}
3) rvalue
void someFunction(std::vector<string>&& items) {
m_items = std::move(items);
}
Какой подход следует избегать и почему?
Ответы
Ответ 1
Если у вас нет причин для того, чтобы вектор находился в куче, я бы посоветовал использовать unique_ptr
В любом случае векторное внутреннее хранилище будет находиться в куче, поэтому вам потребуется 2 степени косвенности, если вы используете unique_ptr
, один для разыменования указателя на вектор и снова для разыменования внутреннего буфера хранения.
Как таковой, я бы посоветовал использовать либо 2, либо 3.
Если вы идете с параметром 3 (требующим ссылки на rvalue), вы накладываете требование на пользователей вашего класса, чтобы они передавали rvalue (либо непосредственно из временного, либо перемещались из lvalue) при вызове someFunction
.
Требование перехода из lvalue является обременительным.
Если ваши пользователи хотят сохранить копию вектора, им нужно перепрыгнуть через обручи, чтобы сделать это.
std::vector<string> items = { "1", "2", "3" };
Test t;
std::vector<string> copy = items; // have to copy first
t.someFunction(std::move(items));
Однако, если вы идете с опцией 2, пользователь может решить, хотят ли они сохранить копию, или нет - выбор принадлежит им
Сохраните копию:
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy
Не сохраняйте копию:
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don't keep a copy
Ответ 2
На поверхности вариант 2 кажется хорошей идеей, поскольку он обрабатывает как lvalues, так и rvalues в одной функции. Однако, как отмечает Herb Sutter в своем интервью CppCon 2014 Вернуться к основам! Основы современного стиля С++, , это пессимизация для общего случая lvalues.
Если m_items
был "больше", чем items
, ваш исходный код не будет выделять память для вектора:
// Original code:
void someFunction(const std::vector<string>& items) {
// If m_items.capacity() >= items.capacity(),
// there is no allocation.
// Copying the strings may still require
// allocations
m_items = items;
}
Оператор присваивания копий на std::vector
достаточно умен, чтобы повторно использовать существующее распределение. С другой стороны, при выборе параметра по значению всегда нужно сделать другое выделение:
// Option 2:
// When passing in an lvalue, we always need to allocate memory and copy over
void someFunction(std::vector<string> items) {
m_items = std::move(items);
}
Проще говоря: копирование конструкции и назначение копии необязательно имеют одинаковую стоимость. Не менее вероятно, что назначение копии будет более эффективным, чем построение копирования — он более эффективен для std::vector
и std::string
†.
Самое простое решение, как отмечает Herb, - добавить перегрузку rvalue (в основном ваш вариант 3):
// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
m_items = std::move(items);
}
Обратите внимание, что оптимизация присваивания копий работает только тогда, когда m_items
уже существует, поэтому определение параметров для конструкторов по значению полностью прекрасное - распределение должно выполняться в любом случае.
TL; DR: Выберите вариант добавления 3. То есть, у вас есть одна перегрузка для lvalues и одна для rvalues. Вариант 2 заставляет копировать конструкцию вместо назначения копии, что может быть дороже (и для std::string
и std::vector
)
† Если вы хотите увидеть тесты, показывающие, что опция 2 может быть пессимизацией, в этот момент в разговоре, Herb показывает некоторые ориентиры
‡ Мы не должны были отмечать это как noexcept
, если std::vector
оператор move-assign не был noexcept
. Проконсультируйтесь с документацией, если вы используете пользовательский распределитель.
Имейте в виду, что аналогичные функции должны быть помечены как noexcept
, если тип move-assign - noexcept
Ответ 3
Это зависит от ваших шаблонов использования:
Вариант 1
Плюсы:
- Ответственность явно выражается и передается от вызывающего абонента к вызываемому абоненту
Минусы:
- Если вектор уже был обернут с помощью
unique_ptr
, это не улучшит читаемость
- Умные указатели в целом управляют динамически выделенными объектами. Таким образом, ваш
vector
должен стать одним. Поскольку стандартными библиотечными контейнерами являются управляемые объекты, которые используют внутренние распределения для хранения своих значений, это означает, что для каждого такого вектора будут два динамических распределения. Один для блока управления уникального объекта ptr + vector
и дополнительного для сохраненных элементов.
Резюме:
Если вы последовательно управляете этим вектором с помощью unique_ptr
, продолжайте использовать его, иначе нет.
Вариант 2
Плюсы:
-
Этот параметр очень гибкий, поскольку он позволяет вызывающему лицу решить, не хранить ли он копию или нет:
std::vector<std::string> vec { ... };
Test t;
t.someFunction(vec); // vec stays a valid copy
t.someFunction(std::move(vec)); // vec is moved
-
Когда вызывающий использует std::move()
, объект перемещается только дважды (без копий), что является эффективным.
Минусы:
- Когда вызывающий абонент не использует
std::move()
, конструктор копирования всегда вызывается для создания временного объекта. Если бы мы использовали void someFunction(const std::vector<std::string> & items)
, а наш m_items
был уже достаточно большим (с точки зрения емкости) для размещения items
, назначение m_items = items
было бы только операцией копирования без дополнительного распределения.
Резюме:
Если вы заранее знаете, что этот объект будет re -set много раз во время выполнения, а вызывающий не всегда использует std::move()
, я бы его избегал. В противном случае это отличный вариант, поскольку он очень гибкий, что позволяет обеспечить удобство для пользователя и повысить производительность по требованию, несмотря на проблемный сценарий.
Вариант 3
Минусы:
-
Этот параметр заставляет вызывающего абонента отказаться от его копии. Поэтому, если он хочет сохранить копию для себя, он должен написать дополнительный код:
std::vector<std::string> vec { ... };
Test t;
t.someFunction(std::vector<std::string>{vec});
Резюме:
Это менее гибко, чем Вариант № 2, и поэтому я бы сказал, что уступает в большинстве сценариев.
Вариант 4
Учитывая недостатки опций 2 и 3, я бы счел, что предлагаю дополнительный вариант:
void someFunction(const std::vector<int>& items) {
m_items = items;
}
// AND
void someFunction(std::vector<int>&& items) {
m_items = std::move(items);
}
Плюсы:
- Он решает все проблемные сценарии, описанные для вариантов 2 и 3, пользуясь также своими преимуществами.
- Caller решил сохранить копию для себя или нет.
- Может быть оптимизирован для любого заданного сценария
Минусы:
- Если метод принимает множество параметров как константных ссылок, так и/или rvalue, число прототипов растет экспоненциально
Резюме:
Пока у вас нет таких прототипов, это отличный вариант.
Ответ 4
Текущий совет по этому вопросу - взять вектор по значению и переместить его в переменную-член:
void fn(std::vector<std::string> val)
{
m_val = std::move(val);
}
И я только что проверил, std::vector
действительно предоставляет оператор переадресации. Если вызывающий абонент не хочет сохранять копию, он может переместить его в функцию на сайте вызова: fn(std::move(vec));
.