[Effective Modern C++] 条款24笔记 - 一个关于std::move与std::forward<T>的小例子

std::move

首先,有如下代码:

class Widget {
public:
void setName(const std::string& newName) // set from
{ name = newName; } // const lvalue
void setName(std::string&& newName) // set from
{ name = std::move(newName); } // rvalue
…
}; 

Widget w;
w.setName("Adela Novak);

如果有上述Widget类,调用w.setName("Adela Novak");语句。调用了哪些函数,实际进行了哪些操作?

在类 Widget 中,存在两个重载的 setName 成员函数,一个接受 const std::string& 参数,另一个接受 std::string&& 参数。它们分别用于处理左值和右值引用。

调用 w.setName("Adela Novak"); 时,会调用如下重载的函数:

void setName(std::string&& newName) // set from
{ name = std::move(newName); } // rvalue

以下是具体的操作过程:

  1. 字符串字面量转换:

    • "Adela Novak" 是一个字符串字面量,它的类型是 const char[12]
    • 在调用 w.setName("Adela Novak"); 时,这个字符串字面量首先被转换为一个临时的 std::string 对象。
  2. 右值引用绑定:

    • 由于临时的 std::string 是一个右值,编译器会选择调用 setName(std::string&& newName) 函数。
    • newName 参数被绑定到这个临时的 std::string 对象。
  3. move 语义:

    • setName(std::string&& newName) 函数体内,调用了 std::move(newName)
    • std::move(newName)newName 转换为一个右值引用。
    • name = std::move(newName)newName 中的资源(如内存缓冲区)移动到 name 成员变量中,而不是复制。

通过 std::movename 成员变量接管了临时 std::string 的资源,从而避免了不必要的复制操作,提高了效率。

调用过程的总结

  • "Adela Novak" 被转换为一个临时的 std::string
  • 临时 std::string 被传递给 setName(std::string&& newName)
  • newName 被移动赋值给 name 成员变量。

这种方式利用了 C++11 引入的右值引用和 move 语义,减少了不必要的资源开销,提高了性能。

std::forward

如果将Widget的实现,更改为模板形式,如下:

class Widget {
public:
    template<typename T>
    void setName(T&& newName) {
        name = std::forward<T>(newName); // Perfect forwarding
    }
private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};

改进后的代码使用了完美转发,通过 std::forward 实现了对左值和右值的正确处理。以下是详细的调用分析:

调用分析

1. 调用 w.setName("Adela Novak");

  1. 模板参数推导:

    • "Adela Novak" 是一个字符串字面量,类型为 const char[12]
    • 模板参数 T 被推导为 const char[12]
    • 因此,T&& 被推导为 const char(&&)[12]
  2. 函数实例化:

    • 实例化后的函数签名为 void setName(const char(&&)[12])
  3. std::forward 转发:

    • std::forward<T>(newName) 等价于 std::forward<const char[12]>(newName)
    • 对于右值引用,std::forward 仍然会返回 newName 的右值引用类型。
    • 然而,std::string 构造函数无法接受一个 const char[12] 右值引用,因此会调用 std::string 的构造函数,传递一个 const char*
  4. std::string 构造函数:

    • name 的赋值操作会调用 std::string 的构造函数,将 const char* 转换为 std::string 对象。

2. 调用 std::string someString = "Adela Novak"; w.setName(someString);

  1. 模板参数推导:

    • someString 是一个左值,类型为 std::string
    • 模板参数 T 被推导为 std::string&
    • 因此,T&& 被推导为 std::string& &&,根据引用折叠规则,简化为 std::string&
  2. 函数实例化:

    • 实例化后的函数签名为 void setName(std::string&)
  3. std::forward 转发:

    • std::forward<T>(newName) 等价于 std::forward<std::string&>(newName)
    • 对于左值引用,std::forward 返回 newName 的左值引用类型。
  4. std::string 赋值操作:

    • name 的赋值操作会调用 std::string 的赋值运算符,将 someString 复制给 name

3. 调用 w.setName(std::move(someString));

  1. 模板参数推导:

    • std::move(someString) 是一个右值,类型为 std::string&&
    • 模板参数 T 被推导为 std::string
    • 因此,T&& 被推导为 std::string&&
  2. 函数实例化:

    • 实例化后的函数签名为 void setName(std::string&&)
  3. std::forward 转发:

    • std::forward<T>(newName) 等价于 std::forward<std::string>(newName)
    • 对于右值引用,std::forward 返回 newName 的右值引用类型。
  4. std::string 赋值操作:

    • name 的赋值操作会调用 std::string 的移动赋值运算符,将 someString 的资源移动给 name,避免了不必要的复制。

总结

  • 左值传递: 当传递左值时,std::forward 保持左值引用,调用复制操作。
  • 右值传递: 当传递右值时,std::forward 转发右值引用,调用移动操作。
  • 字符串字面量: 字符串字面量被正确地转换为 std::string,并使用相应的构造函数进行赋值。

通过使用完美转发,确保了 setName 方法能够高效地处理各种类型的参数传递,既能避免不必要的复制,又能正确处理字符串字面量和其他情况。

posted @ 2024-07-11 15:51  围城chen  阅读(0)  评论(0编辑  收藏  举报