«Текущий совет по этому вопросу:« Я не думаю, что вы обязательно в курсе этого ..

из моих функций берет вектор в качестве параметра и сохраняет его как переменную-член. Я использую постоянную ссылку на вектор, как описано ниже.

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) значение

void someFunction(std::vector<string>&& items) {
   m_items = std::move(items);
}

Какой подход мне следует избегать и почему?

 Steve Lorimer04 окт. 2017 г., 04:58
@xaxxon нет, если вызывающая сторона использует std :: move. Это только поменяет указатель внутреннего буфера на элемент, что очень дешево
 juanchopanza03 окт. 2017 г., 18:18
Все что угодно, кроме unique_ptr.
 NathanOliver03 окт. 2017 г., 18:17
Все зависит от того, как вы хотите, чтобы пользователи класса взаимодействовали с ним. Если вы хотите знать, что они передают вектор вашему классу, вы можете использовать 3 или 1. Если вы хотите, чтобы они сохранили копию вектора, ограничивающую вас 2.

Ответы на вопрос(4)

Решение Вопроса

чтобы вектор жил в куче, я бы посоветовал не использовать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
 Steve Lorimer04 окт. 2017 г., 04:55
@xaxxon, только если пользователь хочет сохранить свою собственную копию. Если нет, они могут использовать std :: move, и вектор будет перемещен, а не скопирован
 HeroicKatora03 окт. 2017 г., 19:46
Если пользователь хочет сохранить копию в случае 3, было бы гораздо удобнее написатьt.someFunction(std::vector<string>{items}); вместо переменной и движется. Я бы сказал, что это будет так же ясно, если не яснее, чем вариант 2.

Опция 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 была бы только операция копирования, без дополнительного выделения.

Резюме:

Если вы заранее знаете, что этот объект будетре-установить много раз во время выполнения, и вызывающая сторона не всегда использует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, в то же время наслаждаясь их преимуществамиЗвонящий решил оставить себе копию или нетМожет быть оптимизирован для любого сценария

Минусы:

Если метод принимает много параметров как константные ссылки, так и / или rvalue ссылается на числопрототипы растет в геометрической прогрессии

Резюме:

Пока у вас нет таких прототипов, это отличный вариант.

 Zebrafish04 окт. 2017 г., 05:08
@Daniel Trugman, передавая std :: move (thevector) в t.someFunction, означает, что он будет перемещен дважды? Один раз, когда вы передаете это и снова внутри someFunction ()?
 Daniel Trugman04 окт. 2017 г., 11:02
@ Zebrafish, я улучшил свой ответ и включил ответ на ваш вопрос (два хода, без копии).
 Daniel Trugman04 окт. 2017 г., 00:49
@StoryTeller, «гибкий» звучит лучше
 StoryTeller03 окт. 2017 г., 18:45
Хороший ответ. Но я думаю, что слово, которое вы ищете, является «гибким», а не «гибким». +1 в любом случае.

вариант 2 кажется хорошей идеей, поскольку он обрабатывает как lvalue, так и rvalues ​​в одной функции. Однако, как отмечает Херб Саттер в своем выступлении на CppCon 2014Вернуться к основам! Основы современного стиля C ++, это пессимизация для общего случая значений l.

Если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 †.

Как отмечает Херб, самое простое решение - добавить перегрузку 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 может быть пессимизацией,в этот момент в разговореТрава показывает некоторые ориентиры

‡ Мы не должны были помечать это какnoexcept еслиstd::vectorОператор перемещения-назначения не былnoexcept, Проконсультироватьсядокументация если вы используете пользовательский распределитель.
Как правило, имейте в виду, что аналогичные функции должны быть отмечены толькоnoexcept если тип перемещения назначенnoexcept

 Justin04 окт. 2017 г., 17:38
@ SteveLorimer Спасибо; Я забыл рассмотреть, что держит вектор.
 Steve Lorimer04 окт. 2017 г., 17:27
> Если m_items был «больше», чем элементы, ваш исходный код не будет выделять память: Это не правильно - он не будет выделять память дляvector, но это очень вероятнобудем выделить память дляstrings

ю-член:

void fn(std::vector<std::string> val)
{
  m_val = std::move(val);
}

И я только что проверил,std::vector действительно предоставляет оператор перемещения-назначения. Если вызывающий не хочет сохранять копию, он может переместить ее в функцию на сайте вызова:fn(std::move(vec));.

 xaxxon04 окт. 2017 г., 11:51
«Текущий совет по этому вопросу:« Я не думаю, что вы обязательно в курсе этого ..

Ваш ответ на вопрос