📜  C++ 实用程序中的 std::move |移动语义、移动构造函数和移动赋值运算符

📅  最后修改于: 2022-05-13 01:54:51.272000             🧑  作者: Mango

C++ 实用程序中的 std::move |移动语义、移动构造函数和移动赋值运算符

先决条件:

  1. 左值引用
  2. 右值引用
  3. 复制语义(复制构造函数)

参考:

在 C++ 中有两种类型的引用 -

  1. 左值参考:
    • 左值是出现在赋值语句左侧或右侧的表达式。
    • 简单地说,就是一个具有名称和内存地址的变量或对象。
    • 它使用一个和号 (&)。
  2. 右值参考:
    • 右值是只出现在赋值语句右侧的表达式。
    • 一个变量或对象只有一个内存地址(临时对象)。
    • 它使用两个与号 (&&)。

移动构造函数和语义:

移动构造函数是在 C++11 中引入的。移动构造函数的需要或目的是尽可能快地从源(原始)对象窃取或移动尽可能多的资源,因为源不再需要具有有意义的值,和/或因为它反正一会儿就毁了。这样就可以避免不必要地创建对象的副本并有效利用资源



虽然可以窃取资源,但必须使源(原始)对象处于可以正确销毁的有效状态。

复制构造函数使用标有一个与号 (&) 的左值引用,而移动构造函数使用标有两个与号 (&&) 的右值引用。

句法:

示例:下面是 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;
}


输出

解释:

假设程序是使用不支持移动语义的编译器编译和执行的。在 main()函数,

1. std::vector vecString;-创建一个空向量,其中没有元素。
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 );-这是一个三阶段的过程-
    1. 一个临时对象将被创建 (str + str) 和它自己单独的内存。
    2. 这个临时对象被插入到再次按值传递的向量 vec 中,这意味着将创建临时字符串对象的(深层)副本。
    3. 截至目前,不再需要临时对象,因此它将被销毁。

注意:在这里,我们不必要地分配和释放临时字符串对象的内存。只需从源对象移动数据,就可以进一步优化(改进)。

  • 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;
}
输出

解释:



在这里,为了使用移动语义。编译器必须支持 C++11 或更高标准。 main()函数和createAndInsert()函数的执行故事在vec.push_back( str );之前保持不变

可能会出现一个问题,为什么不使用 std::move() 将临时对象移动到向量 vec。其背后的原因是向量的 push_back() 方法。从 C++11 开始,push_back() 方法已经提供了新的重载版本。

句法:

  • vec.push_back(str + str);-
    1. 将创建一个临时对象 (str + str) 并使用其自己的单独内存,并将调用重载的 push_back() 方法(版本 3 或 4 取决于 C++ 的版本),该方法将从中窃取(或移动)数据临时源对象 (str + str) 到向量 vec 因为它不再需要。
    2. 执行移动后,临时对象被销毁。因此,它不是调用复制构造函数(复制语义),而是通过复制字符串的大小和操作指向数据内存的指针来优化。
    3. 在这里,需要注意的重要一点是,我们使用了即将不再拥有其内存的内存。换句话说,我们以某种方式对其进行了优化。这完全是因为右值引用和移动语义。
  • vec.push_back(std::move(str));-在 std::move()函数的帮助下,通过转换左值,编译器明确提示“不再需要对象”命名为 str(左值引用)引用到右值引用,并且 str 的资源将被移动到向量。然后 str 的状态变为“有效但未指定的状态”。这对我们来说无关紧要,因为这是我们最后一次使用,无论如何很快就会被销毁。
  • 最后,返回名为 vec 的字符串的局部向量 给它的调用者。
  • 最后,返回到调用者的 main()函数,简单地打印本地 vecString 的元素 向量。

将 vec 对象返回给调用者时可能会出现问题。由于不再需要它,并且将创建向量的整个临时对象,并且局部向量 vec 也将被销毁,那么为什么不使用 std::move() 来窃取值并返回它。
它的答案简单明了,在编译器级别进行了优化,称为(命名)返回值对象,更普遍地称为 RVO。

移动语义的一些后备:

  1. 在 const 对象上调用 std::move() 通常没有效果。
    • 窃取或移动 const 对象的资源没有任何意义。
    • 请参阅下面程序中的constObjectCallFunc()函数
  2. 当且仅当支持复制语义时,复制语义才用作移动语义的后备。
    • 请参阅以下程序中的baz()函数
  3. 如果没有将右值引用作为参数的实现,则将使用普通的 const 左值引用。
    • 请参阅以下程序中的baz()函数
  4. 如果函数或方法丢失,右值引用作为参数 & 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;
}
输出

概括:

  • 移动语义允许我们优化对象的复制,在那里我们不需要价值。它通常隐式地使用(用于未命名的临时对象或本地返回值)或显式地与 std::move() 一起使用。
  • std::move() 表示“不再需要这个值”。
  • 标有 std::move() 的对象永远不会部分销毁。即析构函数将被调用以正确销毁对象。
想要从精选的视频和练习题中学习,请查看C++ 基础课程,从基础到高级 C++ 和C++ STL 课程,了解基础加 STL。要完成从学习语言到 DS Algo 等的准备工作,请参阅完整的面试准备课程