C++ 实用程序中的 std::move |移动语义、移动构造函数和移动赋值运算符
先决条件:
- 左值引用
- 右值引用
- 复制语义(复制构造函数)
参考:
在 C++ 中有两种类型的引用 -
- 左值参考:
- 左值是出现在赋值语句左侧或右侧的表达式。
- 简单地说,就是一个具有名称和内存地址的变量或对象。
- 它使用一个和号 (&)。
- 右值参考:
- 右值是只出现在赋值语句右侧的表达式。
- 一个变量或对象只有一个内存地址(临时对象)。
- 它使用两个与号 (&&)。
移动构造函数和语义:
移动构造函数是在 C++11 中引入的。移动构造函数的需要或目的是尽可能快地从源(原始)对象窃取或移动尽可能多的资源,因为源不再需要具有有意义的值,和/或因为它反正一会儿就毁了。这样就可以避免不必要地创建对象的副本并有效利用资源
虽然可以窃取资源,但必须使源(原始)对象处于可以正确销毁的有效状态。
Move constructors typically “steal” the resource of the source (original) object rather than making several copies of them, and leaves the source object in a “valid but unspecified state”.
复制构造函数使用标有一个与号 (&) 的左值引用,而移动构造函数使用标有两个与号 (&&) 的右值引用。
std::move() is a function used to convert an lvalue reference into the rvalue reference. Used to move the resources from a source object i.e. for efficient transfer of resources from one object to another.
std::move() is defined in the
句法:
template< class T >
typename std::remove_reference
template< class T >
constexpr std::remove_reference_t
示例:下面是 C++ 程序,用于显示在不使用移动语义的情况下会发生什么,即在 C++11 之前。
C++
// C++ program to implement
// the above approach
// for std::string
#include
// for std::cout
#include
// for EXIT_SUCEESS macro
#include
// for std::vector
#include
// Declaration
std::vector createAndInsert();
// Driver cpde
int main()
{
// constructing an empty vector
// of strings
std::vector vecString;
// calling createAndInsert() &
// initializing the local
// vecString object
vecString = createAndInsert();
// Printing content of the vector
for (const auto& s : vecString) {
std::cout << s << '\n';
}
return EXIT_SUCCESS;
}
// Definition
std::vector createAndInsert()
{
// constructing a vector of strings
// with an size of 3 elements
std::vector vec;
vec.reserve(3);
// constructing & intializing a
// string with "Hello"
std::string str("Hello");
// Inserting a copy of string
// object
vec.push_back(str);
// Inserting a copy of an
// temporary string object
vec.push_back(str + str);
// Again inserting a copy of
// string object
// Line 7
vec.push_back(str);
// Finally, returning the local
// vector
return vec;
}
C++14
// C++ program to implement
// the above approach
// for std::string
#include
// for std::cout
#include
// for EXIT_SUCEESS macro
#include
// for std::vector
#include
// for std::move()
#include
// Declaration
std::vector createAndInsert();
// Driver code
int main()
{
// Constructing an empty vector
// of strings
std::vector vecString;
// calling createAndInsert() and
// initializing the local vecString
// object
vecString = createAndInsert();
// Printing content of the vector
for (const auto& s : vecString) {
std::cout << s << '\n';
}
return EXIT_SUCCESS;
}
// Definition
std::vector createAndInsert()
{
// constructing a vector of
// strings with an size of
// 3 elements
std::vector vec;
vec.reserve(3);
// constructing & intializing
// a string with "Hello"
std::string str("Hello");
// Inserting a copy of string
// object
vec.push_back(str);
// Inserting a copy of an
// temporary string object
vec.push_back(str + str);
// Again inserting a copy of
// string object
vec.push_back(std::move(str));
// Finally, returning the local
// vector
return vec;
}
C++14
// C++ program to implement
// the above concept
// for std::cout & std::endl
#include
// for std::move()
#include
// for std::string
#include
// for EXIT_SUCCESS macro
#include
// foo() taking a non-const lvalue
// reference argument
void foo(std::string& str);
// foo() taking a const lvalue
// reference argument
void foo(const std::string& str);
// foo() taking a rvalue
// reference argument
void foo(std::string&& str);
// baz() taking a const lvalue
// reference argument
void baz(const std::string& str);
// baz() taking a non-const lvalue
// reference argument
void baz(std::string& str);
// bar() taking a non-const lvalue
// reference argument
void bar(std::string& str);
// constObjectCallFunc() taking a
// rvalue reference argument
void constObjectCallFunc(std::string&& str);
// Driver cpde
int main()
{
// foo(std::string&& str) will
// be called
foo(std::string("Hello"));
std::string goodBye("Good Bye!");
// foo(std::string& str) will be called
foo(goodBye);
// foo(std::string&& str) will be called
foo(std::move(goodBye + " using std::move()"));
std::cout << "\n\n\n";
// move semantics fallback
// baz(const std::string& str) will be called
baz(std::string("This is temporary string object"));
// baz(const std::string& str) will be called
baz(std::move(std::string(
"This is temporary string object using std::move()")));
std::cout << "\n\n\n";
std::string failToCall("This will fail to call");
/*
Reasons to fail bar() call -
1. No rvalue reference implementation
available // First Preference
2. No const lvalue refernce implementation
available // Second Preference
3. Finally fails to invoke bar() function
*/
// bar(std::move(failToCall));
// Error : check the error message for more
// better understanding
std::cout << "\n\n\n";
const std::string constObj(
"Calling a std::move() on a const object usually has no effect.");
// constObjectCallFunc(std::move(constObj));
// Error : because of const qualifier
// It doesn't make any sense to steal or
// move the resources of a const object
return EXIT_SUCCESS;
}
void foo(const std::string& str)
{
// do something
std::cout << "foo(const std::string& str) : "
<< "\n\t" << str << std::endl;
}
void foo(std::string& str)
{
// do something
std::cout << "foo(std::string& str) : "
<< "\n\t" << str << std::endl;
}
void foo(std::string&& str)
{
// do something
std::cout << "foo(std::string&& str) : "
<< "\n\t" << str << std::endl;
}
void baz(const std::string& str)
{
// do something
std::cout << "baz(const std::string& str) : "
<< "\n\t" << str << std::endl;
}
void baz(std::string& str)
{
// do something
std::cout << "baz(std::string& str) : "
<< "\n\t" << str << std::endl;
}
void bar(std::string& str)
{
// do something
std::cout << "bar(std::string&& str) : "
<< "\n\t" << str << std::endl;
}
void constObjectCallFunc(std::string&& str)
{
// do something
std::cout << "constObjectCallFunc(std::string&& str) : "
<< "\n\t" << str << std::endl;
}
Hello
HelloHello
Hello
解释:
假设程序是使用不支持移动语义的编译器编译和执行的。在 main()函数,
1. std::vector
2. vecString = createAndInsert();-调用createAndInsert()函数。
3. 在 createAndInsert()函数-
- std:: 字符串 str(“Hello”);-另一个名为 vec 的新空向量被创建。
- vec.reserve(3);-保留 3 个元素的大小。
- 初始化为“你好”命名为STR的字符串- ;的std :: 字符串 STR(“你好”)。
- vec.push_back( str );-将字符串按值传递给向量 vec。因此,将创建 str 的(深层)副本并将其插入到 vec 中 通过调用 String 类的复制构造函数。
- vec.push_back( str + str );-这是一个三阶段的过程-
- 一个临时对象将被创建 (str + str) 和它自己单独的内存。
- 这个临时对象被插入到再次按值传递的向量 vec 中,这意味着将创建临时字符串对象的(深层)副本。
- 截至目前,不再需要临时对象,因此它将被销毁。
注意:在这里,我们不必要地分配和释放临时字符串对象的内存。只需从源对象移动数据,就可以进一步优化(改进)。
- vec.push_back( str );- 与第 1 行相同的过程。 5将进行。请记住此时将最后使用 str字符串对象。
- return vec;-这是在 createAndInsert()函数的末尾-
- 首先,字符串对象str 将被销毁,因为作用域被保留在它被声明的地方。
- 其次,返回字符串的局部向量,即 vec。由于函数的返回类型不是引用。因此,将通过在单独的内存位置进行分配来创建整个向量的深层副本,然后销毁本地 vec 对象,因为作用域保留在声明它的位置。
- 最后,字符串向量的副本将返回给调用者 main()函数。
- 最后,在返回调用者 main()函数,简单地打印本地 vecString 向量的元素。
示例:下面是使用移动语义实现上述概念的 C++ 程序,即自 C++11 及更高版本。
C++14
// C++ program to implement
// the above approach
// for std::string
#include
// for std::cout
#include
// for EXIT_SUCEESS macro
#include
// for std::vector
#include
// for std::move()
#include
// Declaration
std::vector createAndInsert();
// Driver code
int main()
{
// Constructing an empty vector
// of strings
std::vector vecString;
// calling createAndInsert() and
// initializing the local vecString
// object
vecString = createAndInsert();
// Printing content of the vector
for (const auto& s : vecString) {
std::cout << s << '\n';
}
return EXIT_SUCCESS;
}
// Definition
std::vector createAndInsert()
{
// constructing a vector of
// strings with an size of
// 3 elements
std::vector vec;
vec.reserve(3);
// constructing & intializing
// a string with "Hello"
std::string str("Hello");
// Inserting a copy of string
// object
vec.push_back(str);
// Inserting a copy of an
// temporary string object
vec.push_back(str + str);
// Again inserting a copy of
// string object
vec.push_back(std::move(str));
// Finally, returning the local
// vector
return vec;
}
Hello
HelloHello
Hello
解释:
在这里,为了使用移动语义。编译器必须支持 C++11 或更高标准。 main()函数和createAndInsert()函数的执行故事在vec.push_back( str );行之前保持不变。
可能会出现一个问题,为什么不使用 std::move() 将临时对象移动到向量 vec。其背后的原因是向量的 push_back() 方法。从 C++11 开始,push_back() 方法已经提供了新的重载版本。
句法:
constexpr void push_back(const T& value); (since C++20)
void push_back(T&& value); (since C++11) (until C++20)
void push_back(const T& value); (until C++20)
constexpr void push_back(T&& value); (since C++20)
- vec.push_back(str + str);-
- 将创建一个临时对象 (str + str) 并使用其自己的单独内存,并将调用重载的 push_back() 方法(版本 3 或 4 取决于 C++ 的版本),该方法将从中窃取(或移动)数据临时源对象 (str + str) 到向量 vec 因为它不再需要。
- 执行移动后,临时对象被销毁。因此,它不是调用复制构造函数(复制语义),而是通过复制字符串的大小和操作指向数据内存的指针来优化。
- 在这里,需要注意的重要一点是,我们使用了即将不再拥有其内存的内存。换句话说,我们以某种方式对其进行了优化。这完全是因为右值引用和移动语义。
- vec.push_back(std::move(str));-在 std::move()函数的帮助下,通过转换左值,编译器明确提示“不再需要对象”命名为 str(左值引用)引用到右值引用,并且 str 的资源将被移动到向量。然后 str 的状态变为“有效但未指定的状态”。这对我们来说无关紧要,因为这是我们最后一次使用,无论如何很快就会被销毁。
- 最后,返回名为 vec 的字符串的局部向量 给它的调用者。
- 最后,返回到调用者的 main()函数,简单地打印本地 vecString 的元素 向量。
将 vec 对象返回给调用者时可能会出现问题。由于不再需要它,并且将创建向量的整个临时对象,并且局部向量 vec 也将被销毁,那么为什么不使用 std::move() 来窃取值并返回它。
它的答案简单明了,在编译器级别进行了优化,称为(命名)返回值对象,更普遍地称为 RVO。
移动语义的一些后备:
- 在 const 对象上调用 std::move() 通常没有效果。
- 窃取或移动 const 对象的资源没有任何意义。
- 请参阅下面程序中的constObjectCallFunc()函数
- 当且仅当支持复制语义时,复制语义才用作移动语义的后备。
- 请参阅以下程序中的baz()函数
- 如果没有将右值引用作为参数的实现,则将使用普通的 const 左值引用。
- 请参阅以下程序中的baz()函数
- 如果函数或方法丢失,右值引用作为参数 & const 左值引用作为参数。然后会产生编译时错误。
- 请参阅以下程序中的bar()函数
笔记:
foo()函数具有所有必需的参数类型。
以下是实现上述所有概念的 C++ 程序-
C++14
// C++ program to implement
// the above concept
// for std::cout & std::endl
#include
// for std::move()
#include
// for std::string
#include
// for EXIT_SUCCESS macro
#include
// foo() taking a non-const lvalue
// reference argument
void foo(std::string& str);
// foo() taking a const lvalue
// reference argument
void foo(const std::string& str);
// foo() taking a rvalue
// reference argument
void foo(std::string&& str);
// baz() taking a const lvalue
// reference argument
void baz(const std::string& str);
// baz() taking a non-const lvalue
// reference argument
void baz(std::string& str);
// bar() taking a non-const lvalue
// reference argument
void bar(std::string& str);
// constObjectCallFunc() taking a
// rvalue reference argument
void constObjectCallFunc(std::string&& str);
// Driver cpde
int main()
{
// foo(std::string&& str) will
// be called
foo(std::string("Hello"));
std::string goodBye("Good Bye!");
// foo(std::string& str) will be called
foo(goodBye);
// foo(std::string&& str) will be called
foo(std::move(goodBye + " using std::move()"));
std::cout << "\n\n\n";
// move semantics fallback
// baz(const std::string& str) will be called
baz(std::string("This is temporary string object"));
// baz(const std::string& str) will be called
baz(std::move(std::string(
"This is temporary string object using std::move()")));
std::cout << "\n\n\n";
std::string failToCall("This will fail to call");
/*
Reasons to fail bar() call -
1. No rvalue reference implementation
available // First Preference
2. No const lvalue refernce implementation
available // Second Preference
3. Finally fails to invoke bar() function
*/
// bar(std::move(failToCall));
// Error : check the error message for more
// better understanding
std::cout << "\n\n\n";
const std::string constObj(
"Calling a std::move() on a const object usually has no effect.");
// constObjectCallFunc(std::move(constObj));
// Error : because of const qualifier
// It doesn't make any sense to steal or
// move the resources of a const object
return EXIT_SUCCESS;
}
void foo(const std::string& str)
{
// do something
std::cout << "foo(const std::string& str) : "
<< "\n\t" << str << std::endl;
}
void foo(std::string& str)
{
// do something
std::cout << "foo(std::string& str) : "
<< "\n\t" << str << std::endl;
}
void foo(std::string&& str)
{
// do something
std::cout << "foo(std::string&& str) : "
<< "\n\t" << str << std::endl;
}
void baz(const std::string& str)
{
// do something
std::cout << "baz(const std::string& str) : "
<< "\n\t" << str << std::endl;
}
void baz(std::string& str)
{
// do something
std::cout << "baz(std::string& str) : "
<< "\n\t" << str << std::endl;
}
void bar(std::string& str)
{
// do something
std::cout << "bar(std::string&& str) : "
<< "\n\t" << str << std::endl;
}
void constObjectCallFunc(std::string&& str)
{
// do something
std::cout << "constObjectCallFunc(std::string&& str) : "
<< "\n\t" << str << std::endl;
}
foo(std::string&& str) :
Hello
foo(std::string& str) :
Good Bye!
foo(std::string&& str) :
Good Bye! using std::move()
baz(const std::string& str) :
This is temporary string object
baz(const std::string& str) :
This is temporary string object using std::move()
概括:
- 移动语义允许我们优化对象的复制,在那里我们不需要价值。它通常隐式地使用(用于未命名的临时对象或本地返回值)或显式地与 std::move() 一起使用。
- std::move() 表示“不再需要这个值”。
- 标有 std::move() 的对象永远不会部分销毁。即析构函数将被调用以正确销毁对象。