C++
Переместить семантику
Поиск…
Переместить семантику
Перенос семантики - это способ перемещения одного объекта на другой в C ++. Для этого мы очищаем старый объект и помещаем все, что было в новом объекте.
Для этого мы должны понять, что такое ссылка rvalue. Ссылка rvalue ( T&&
где T - тип объекта) не сильно отличается от обычной ссылки ( T&
, теперь называемой ссылкой lvalue). Но они действуют как два разных типа, и поэтому мы можем создавать конструкторы или функции, которые принимают один тип или другой, что необходимо при использовании семантики перемещения.
Причина, по которой нам нужны два разных типа, - это указать два разных поведения. Конструкторы ссылок Lvalue связаны с копированием, а конструкторы ссылок rvalue связаны с перемещением.
Чтобы переместить объект, мы будем использовать std::move(obj)
. Эта функция возвращает ссылку на объект rvalue, чтобы мы могли украсть данные из этого объекта в новый. Существует несколько способов сделать это, которые обсуждаются ниже.
Важно отметить, что использование std::move
создает только ссылку rvalue. Другими словами, оператор std::move(obj)
не изменяет содержимое obj, тогда как auto obj2 = std::move(obj)
(возможно).
Переместить конструктор
Скажем, у нас есть этот фрагмент кода.
class A {
public:
int a;
int b;
A(const A &other) {
this->a = other.a;
this->b = other.b;
}
};
Чтобы создать конструктор копирования, то есть создать функцию, которая копирует объект и создает новую, мы обычно выбираем синтаксис, показанный выше, у нас будет конструктор для A, который ссылается на другой объект типа A, и мы будем копировать объект вручную внутри метода.
В качестве альтернативы мы могли бы написать A(const A &) = default;
который автоматически копирует все элементы, используя его конструктор копирования.
Однако для создания конструктора перемещения мы будем использовать ссылку rvalue вместо ссылки lvalue, как здесь.
class Wallet {
public:
int nrOfDollars;
Wallet() = default; //default ctor
Wallet(Wallet &&other) {
this->nrOfDollars = other.nrOfDollars;
other.nrOfDollars = 0;
}
};
Обратите внимание, что мы установили старые значения в zero
. Конструктор перемещения по умолчанию ( Wallet(Wallet&&) = default;
) копирует значение nrOfDollars
, так как это POD.
Поскольку семантика перемещения предназначена для того, чтобы разрешить «кражи» состояния из исходного экземпляра, важно рассмотреть, как должен выглядеть исходный экземпляр после этого кражи. В этом случае, если бы мы не изменили значение до нуля, мы бы удвоили сумму долларов в игре.
Wallet a;
a.nrOfDollars = 1;
Wallet b (std::move(a)); //calling B(B&& other);
std::cout << a.nrOfDollars << std::endl; //0
std::cout << b.nrOfDollars << std::endl; //1
Таким образом, мы перемещаем объект из старого.
Хотя приведенный выше пример является простым, он показывает, что должен сделать конструктор перемещения. Он становится более полезным в более сложных случаях, например, при управлении ресурсами.
// Manages operations involving a specified type.
// Owns a helper on the heap, and one in its memory (presumably on the stack).
// Both helpers are DefaultConstructible, CopyConstructible, and MoveConstructible.
template<typename T,
template<typename> typename HeapHelper,
template<typename> typename StackHelper>
class OperationsManager {
using MyType = OperationsManager<T, HeapHelper, StackHelper>;
HeapHelper<T>* h_helper;
StackHelper<T> s_helper;
// ...
public:
// Default constructor & Rule of Five.
OperationsManager() : h_helper(new HeapHelper<T>) {}
OperationsManager(const MyType& other)
: h_helper(new HeapHelper<T>(*other.h_helper)), s_helper(other.s_helper) {}
MyType& operator=(MyType copy) {
swap(*this, copy);
return *this;
}
~OperationsManager() {
if (h_helper) { delete h_helper; }
}
// Move constructor (without swap()).
// Takes other's HeapHelper<T>*.
// Takes other's StackHelper<T>, by forcing the use of StackHelper<T>'s move constructor.
// Replaces other's HeapHelper<T>* with nullptr, to keep other from deleting our shiny
// new helper when it's destroyed.
OperationsManager(MyType&& other) noexcept
: h_helper(other.h_helper),
s_helper(std::move(other.s_helper)) {
other.h_helper = nullptr;
}
// Move constructor (with swap()).
// Places our members in the condition we want other's to be in, then switches members
// with other.
// OperationsManager(MyType&& other) noexcept : h_helper(nullptr) {
// swap(*this, other);
// }
// Copy/move helper.
friend void swap(MyType& left, MyType& right) noexcept {
std::swap(left.h_helper, right.h_helper);
std::swap(left.s_helper, right.s_helper);
}
};
Переместить назначение
Подобно тому, как мы можем присвоить значение объекту с ссылкой lvalue, скопировав его, мы также можем перемещать значения из объекта в другой, не создавая новый. Мы называем это назначение перемещения. Мы перемещаем значения из одного объекта в другой существующий объект.
Для этого нам придется перегружать operator =
, а не так, чтобы он ссылался на ссылку lvalue, как и при копировании, но так, чтобы он принимал ссылку rvalue.
class A {
int a;
A& operator= (A&& other) {
this->a = other.a;
other.a = 0;
return *this;
}
};
Это типичный синтаксис для определения перемещения. Мы перегружаем operator =
так, чтобы мы могли подать ему ссылку на rvalue и присвоить ее другому объекту.
A a;
a.a = 1;
A b;
b = std::move(a); //calling A& operator= (A&& other)
std::cout << a.a << std::endl; //0
std::cout << b.a << std::endl; //1
Таким образом, мы можем переместить назначение объекта другому.
Использование std :: move для уменьшения сложности от O (n²) до O (n)
C ++ 11 представил базовый язык и стандартную библиотечную поддержку для перемещения объекта. Идея заключается в том , что , когда объект о является временным , и один хочет логическую копию, то ее безопасно только стянуть о «s ресурсы, такие как динамически выделенный буфер, оставляя о логически пустым , но по- прежнему разрушаемость и копируемой.
Поддержка основного языка в основном
конструктор ссылочного типа rvalue
&&
, напримерstd::string&&
является ссылкой rvalue наstd::string
, указывая на то, что упомянутый объект является временным, ресурсы которого могут быть просто украдены (т.е. перемещены)специальную поддержку для конструктора перемещения
T( T&& )
, который должен эффективно перемещать ресурсы из указанного другого объекта, а не фактически копировать эти ресурсы, испециальная поддержка оператора
auto operator=(T&&) -> T&
присваивания оператораauto operator=(T&&) -> T&
, который также должен перемещаться из источника.
Стандартная поддержка библиотеки - это главным образом шаблон std::move
из заголовка <utility>
. Эта функция создает ссылку rvalue для указанного объекта, указывая на то, что она может быть перемещена, как если бы она была временной.
Для контейнера фактическое копирование обычно имеет сложность O ( n ), где n - количество элементов в контейнере, а перемещение - O (1), постоянное время. А для алгоритма, который логически копирует этот контейнер n раз, это может уменьшить сложность от обычно непрактичного O ( n ²) до линейного O ( n ).
В своей статье «Контейнеры, которые никогда не меняются» в Dr. Dobbs Journal в сентябре 19 2013 года Эндрю Кениг представил интересный пример алгоритмической неэффективности при использовании стиля программирования, где переменные неизменны после инициализации. С помощью этого стиля петли обычно выражаются с помощью рекурсии. И для некоторых алгоритмов, таких как создание последовательности Collatz, рекурсия требует логического копирования контейнера:
// Based on an example by Andrew Koenig in his Dr. Dobbs Journal article
// “Containers That Never Change” September 19, 2013, available at
// <url: http://www.drdobbs.com/cpp/containters-that-never-change/240161543>
// Includes here, e.g. <vector>
namespace my {
template< class Item >
using Vector_ = /* E.g. std::vector<Item> */;
auto concat( Vector_<int> const& v, int const x )
-> Vector_<int>
{
auto result{ v };
result.push_back( x );
return result;
}
auto collatz_aux( int const n, Vector_<int> const& result )
-> Vector_<int>
{
if( n == 1 )
{
return result;
}
auto const new_result = concat( result, n );
if( n % 2 == 0 )
{
return collatz_aux( n/2, new_result );
}
else
{
return collatz_aux( 3*n + 1, new_result );
}
}
auto collatz( int const n )
-> Vector_<int>
{
assert( n != 0 );
return collatz_aux( n, Vector_<int>() );
}
} // namespace my
#include <iostream>
using namespace std;
auto main() -> int
{
for( int const x : my::collatz( 42 ) )
{
cout << x << ' ';
}
cout << '\n';
}
Выход:
42 21 64 32 16 8 4 2
Количество операций копирования элементов из-за копирования векторов здесь примерно равно O ( n² ), так как это сумма 1 + 2 + 3 + ... n .
В конкретных числах, с компиляторами g ++ и Visual C ++, вышеупомянутый вызов collatz(42)
привел к последовательности Collatz из 8 элементов и 36 операций копирования экземпляров (8 * collatz(42)
= 28, плюс некоторые) в вызовах конструктора вектора.
Все эти операции копирования элементов можно удалить простым переносом векторов, значения которых больше не нужны. Для этого необходимо удалить const
и ссылку для аргументов типа вектора, передав векторы по значению . Возврат функции автоматически оптимизирован. Для вызовов, где векторы передаются и не используются снова в функции, просто примените std::move
чтобы переместить эти буферы, а не копировать их:
using std::move;
auto concat( Vector_<int> v, int const x )
-> Vector_<int>
{
v.push_back( x );
// warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
// See https://stackoverflow.com/documentation/c%2b%2b/2489/copy-elision
// return move( v );
return v;
}
auto collatz_aux( int const n, Vector_<int> result )
-> Vector_<int>
{
if( n == 1 )
{
return result;
}
auto new_result = concat( move( result ), n );
struct result; // Make absolutely sure no use of `result` after this.
if( n % 2 == 0 )
{
return collatz_aux( n/2, move( new_result ) );
}
else
{
return collatz_aux( 3*n + 1, move( new_result ) );
}
}
auto collatz( int const n )
-> Vector_<int>
{
assert( n != 0 );
return collatz_aux( n, Vector_<int>() );
}
Здесь, с компиляторами g ++ и Visual C ++, количество операций копирования элементов из-за вызовов конструктора векторных копий было ровно 0.
Алгоритм обязательно должен быть O ( n ) в длине последовательности Collatz, но это довольно значительное улучшение: O ( n ²) → O ( n ).
При некоторой поддержке языка можно было бы использовать перемещение и все еще выражать и обеспечивать неизменность переменной между ее инициализацией и конечным ходом , после чего любое использование этой переменной должно быть ошибкой. Увы, с C ++ 14 C ++ этого не поддерживает. Для кода без цикла без использования после перемещения может быть принудительно введено повторное объявление соответствующего имени как неполной struct
, как и для struct result;
выше, но это уродливо и вряд ли будет понято другими программистами; также диагностика может быть довольно вводящей в заблуждение.
Подводя итоги, поддержка языка C ++ и библиотеки для перемещения позволяет радикально улучшить сложность алгоритма, но из-за неполноты поддержки за счет отказа от гарантий правильности кода и ясности кода, которые может обеспечить const
.
Для полноты, инструментальный векторный класс, используемый для измерения количества операций копирования элементов из-за вызовов конструктора копирования:
template< class Item >
class Copy_tracking_vector
{
private:
static auto n_copy_ops()
-> int&
{
static int value;
return value;
}
vector<Item> items_;
public:
static auto n() -> int { return n_copy_ops(); }
void push_back( Item const& o ) { items_.push_back( o ); }
auto begin() const { return items_.begin(); }
auto end() const { return items_.end(); }
Copy_tracking_vector(){}
Copy_tracking_vector( Copy_tracking_vector const& other )
: items_( other.items_ )
{ n_copy_ops() += items_.size(); }
Copy_tracking_vector( Copy_tracking_vector&& other )
: items_( move( other.items_ ) )
{}
};
Использование семантики перемещения на контейнерах
Вы можете переместить контейнер вместо копирования:
void print(const std::vector<int>& vec) {
for (auto&& val : vec) {
std::cout << val << ", ";
}
std::cout << std::endl;
}
int main() {
// initialize vec1 with 1, 2, 3, 4 and vec2 as an empty vector
std::vector<int> vec1{1, 2, 3, 4};
std::vector<int> vec2;
// The following line will print 1, 2, 3, 4
print(vec1);
// The following line will print a new line
print(vec2);
// The vector vec2 is assigned with move assingment.
// This will "steal" the value of vec1 without copying it.
vec2 = std::move(vec1);
// Here the vec1 object is in an indeterminate state, but still valid.
// The object vec1 is not destroyed,
// but there's is no guarantees about what it contains.
// The following line will print 1, 2, 3, 4
print(vec2);
}
Повторное использование перемещенного объекта
Вы можете повторно использовать перемещенный объект:
void consumingFunction(std::vector<int> vec) {
// Some operations
}
int main() {
// initialize vec with 1, 2, 3, 4
std::vector<int> vec{1, 2, 3, 4};
// Send the vector by move
consumingFunction(std::move(vec));
// Here the vec object is in an indeterminate state.
// Since the object is not destroyed, we can assign it a new content.
// We will, in this case, assign an empty value to the vector,
// making it effectively empty
vec = {};
// Since the vector as gained a determinate value, we can use it normally.
vec.push_back(42);
// Send the vector by move again.
consumingFunction(std::move(vec));
}