Это правильное использование семантики С++ "move"?
Сегодня я просматриваю код, над которым я работал последние несколько дней, и начал читать семантику перемещения, в частности std:: move. У меня есть несколько вопросов, чтобы спросить вас, за плюсом, чтобы я пошел по правильному пути и не делал глупых предположений!
Во-первых:
1) Первоначально мой код имел функцию, которая возвращала большой вектор:
template<class T> class MyObject
{
public:
std::vector<T> doSomething() const;
{
std::vector<T> theVector;
// produce/work with a vector right here
return(theVector);
}; // eo doSomething
}; // eo class MyObject
Учитывая, что "theVector" является временным в этом и "отбрасыванием", я изменил функцию на:
std::vector<T>&& doSomething() const;
{
std::vector<T> theVector;
// produce/work with a vector right here
return(static_cast<std::vector<T>&&>(theVector));
}; // eo doSomething
Это правильно? Любые подводные камни в этом случае?
2) Я заметил, что в функции, которая у меня есть, возвращается std::string
, которая автоматически называется конструктором перемещения. Отладка в Return of String (спасибо, Aragorn), я заметил, что он называется явным конструктором перемещения. Почему существует один для класса строк, а не для вектора?
Мне не нужно было вносить какие-либо изменения в эту функцию, чтобы воспользоваться семантикой перемещения:
// below, no need for std::string&& return value?
std::string AnyConverter::toString(const boost::any& _val) const
{
string ret;
// convert here
return(ret); // No need for static_cast<std::string&&> ?
}; // eo toString
3) Наконец, я хотел выполнить некоторые тесты производительности, это удивительно быстрые результаты, которые я получил из-за std:: move семантики или мой компилятор (VS2010) тоже сделал некоторую оптимизацию?
(Реализация _getMilliseconds()
опущена для краткости)
std::vector<int> v;
for(int a(0); a < 1000000; ++a)
v.push_back(a);
std::vector<int> x;
for(int a(0); a < 1000000; ++a)
x.push_back(a);
int s1 = _getMilliseconds();
std::vector<int> v2 = v;
int s2 = _getMilliseconds();
std::vector<int> v3 = std::move(x);
int s3 = _getMilliseconds();
int result1 = s2 - s1;
int result2 = s3 - s2;
Результаты были, очевидно, удивительными. result1, стандартное задание, заняло 630 мс. Второй результат - 0мс. Является ли это хорошей проверкой производительности этих вещей?
Я знаю, что некоторые из них очевидны для многих из вас, но я хочу, чтобы убедиться, что я понимаю семантику прямо перед тем, как я запустил блейзер в свой код.
Спасибо заранее!
Ответы
Ответ 1
Ссылка по-прежнему является ссылкой. Точно так же вы не можете вернуть ссылку на локальный в С++ 03 (или вы получаете UB), вы не можете в С++ 0x. Вы получите ссылку на мертвый объект; это просто ссылка rvalue. Так что это неправильно:
std::vector<T>&& doSomething() const
{
std::vector<T> local;
return local; // oops
return std::move(local); // also oops
}
Вы должны просто делать то, что вы видели во втором номере:
// okay, return by-value
std::vector<T> doSomething() const
{
std::vector<T> local;
return local; // exactly the same as:
return std::move(local); // move-construct value
}
Переменные, локальные для функции, являются временными, когда вы возвращаетесь, поэтому нет необходимости изменять какой-либо код. Тип возврата - это предмет, ответственный за реализацию семантики перемещения, а не вас.
Вы хотите использовать std::move
для явного перемещения чего-либо, когда это не будет сделано нормально, как в вашем тесте. (Кажется, все в порядке, это было в Release? Вы должны вывести содержимое вектора, или компилятор будет оптимизировать его.)
Если вы хотите узнать о ссылках на rvalue, прочитать это.
Ответ 2
return(theVector);
Это уже движется неявно из-за специального правила языка, потому что theVector
является локальным объектом. См. Параграфы 34 и 35 раздела 12.8:
Когда определенные критерии выполняются, реализации разрешается опускать конструкцию копирования/перемещения класса object, даже если конструктор copy/move и/или деструктор для объекта имеют побочные эффекты. В таких случаях, реализация рассматривает источник и цель пропущенной операции копирования/перемещения как просто две разные способы обращения к одному и тому же объекту, а уничтожение этого объекта происходит в более поздние времена когда два объекта были бы уничтожены без оптимизации. Это разрешение копирования/перемещения операции, называемые копией, разрешены в следующих обстоятельствах (которые могут быть объединены с устранить несколько копий):
- в операторе return в функции с возвращаемым типом класса, когда выражение является именем энергонезависимый автоматический объект с тем же самым cv-неквалифицированным типом, что и тип возврата функции, операция копирования/перемещения может быть опущена путем создания автоматического объекта непосредственно в функции возвращаемое значение
[...]
Когда критерии для выполнения операции копирования выполняются, и объект, который нужно скопировать, обозначается символом lvalue, разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначается rvalue.
Обратите внимание, что вы должны вернуть std::vector<T>
(по значению), не a std::vector<T>&&
(по ссылке).
Но почему скобки? return
не является функцией:
return theVector;
Ответ 3
Чтобы добавить к GMan ответ: даже если вы измените тип возвращаемого значения на std::vector<T>
(без каких-либо ссылок, иначе вы получите UB), ваша замена выражения return в "1" "никогда не улучшит производительность, но может сделать это немного хуже. Поскольку std::vector
имеет конструктор перемещения, и вы возвращаете локальный объект, конструктор копирования vector
не будет вызываться, независимо от того, вы написали return theVector;
, return static_cast<std::vector<T>&&>(theVector);
или return std::move(theVector)
. В последних двух случаях компилятор будет вынужден вызвать конструктор перемещения. Но в первом случае у него есть свобода для оптимизации всего движения, если он может сделать NRVO для этой функции. Если по какой-то причине NRVO невозможно, только тогда компилятор прибегает к вызову конструктора перемещения. Поэтому не меняйте return x;
на return std::move(x);
, если x
- это локальный нестатический объект в возвращаемой функции, иначе вы не сможете использовать другую возможность оптимизации.
Ответ 4
Стандартный способ перемещения чего-то - с std::move(x)
, а не static_cast
. AFAIK, оптимизация с наименьшим значением возвращаемого значения, скорее всего, начнет возвращать вектор по значению, поэтому он выполнил бы и перед семантикой переноса.
Ваш тест производительности является хорошей иллюстрацией того, как семантика переноса хороша для производительности: присваивание копий должно копировать миллион элементов, а присваивание move существенно просто меняет векторные внутренние указатели вокруг, что представляет собой тривиальное назначение слов или два, всего несколько циклов.