您的位置:首页 > 教育 > 培训 > C++编程:理解右值引用(C++ 11及以上版本):

C++编程:理解右值引用(C++ 11及以上版本):

2024/10/5 17:18:03 来源:https://blog.csdn.net/ComputerInBook/article/details/141928700  浏览:    关键词:C++编程:理解右值引用(C++ 11及以上版本):

                                    右值引用(rvalue reference)

目录

1. 概述

2. 右值引用的语法格式

3. 移动语义(semantics)(语义:文字表达结合语境的含义)

4.  完备转交(Perfect forwarding)

5.  右值引用之属性

6.  总结


1. 概述

    C++表达式有两个属性:类型和类型分类。虽然类型的更详细的分类不只2个,但在应用开发中,我们可以简单地认为类型分类只有左值和右值。这个分类有助于编译器确定表达式是否为值、函数、或对象。简单说,左值就是可以用程序取得其地址并进行访问的对象,而非左值即为右值。

    C++11 添加了一种称为右值引用的新引用类型。右值引用是一种设计为仅用右值进行初始化的引用。左值引用和右值引用在语法和语义上相似,但它们遵循的规则略有不同。左值引用使用单个 & 符号创建,而右值引用使用两个 & 符号(&&)创建。

2. 右值引用的语法格式

右值引用类型id:

            类型指定符序列 &&    属性指定符序列(可选)    指针抽象声明符(可选)。

3. 移动语义(semantics)(语义:文字表达结合语境的含义)

    右值引用支持移动语义(move semantics)的实现,这可以显著地提高应用程序的性能。移动语义使您能够编写将资源(例如动态分配的内存)从一个对象转移到另一个对象的代码。移动语义之所以有效,是因为它支持从临时对象(程序中他处无法引用的对象)转移资源。

    要实现移动语义,通常需要为类提供一个移动构造函数,以及可选的移动赋值运算符 (operator=)。然后,移动源为右值的复制和赋值操作将自动利用移动语义。与默认复制构造函数不同,编译器不提供默认移动构造函数。有关如何编写和使用移动构造函数的详细信息,请参阅移动构造函数和移动赋值运算符。

    您还可以重载普通函数和运算符以利用移动语义。例如,字符串类实现了使用移动语义的操作。请考虑以下连接多个字符串并打印结果的示例:

// string_concatenation.cpp

// compile with: /EHsc

#include <iostream>

#include <string>

using namespace std;

int main()

{

    string s = string("h") + "e" + "ll" + "o";

    cout << s << endl;

}

    在 Visual Studio 2010 之前,每次调用 operator+ 都会分配并返回一个新的临时字符串对象(右值)。operator+ 无法将一个字符串附加到另一个字符串,因为它不知道源字符串是左值还是右值。如果源字符串都是左值,则它们可能会在程序的其他位置被引用,因此不能修改。您可以修改 operator+ 以使用右值引用来获取右值,而右值引用不能在程序的其他位置被引用。通过此更改,operator+ 现在可以将一个字符串附加到另一个字符串。此更改显著减少了字符串类必须进行的动态内存分配次数。有关字符串类的更多信息,请参阅 basic_string 类。

    当编译器无法使用返回值优化 (RVO) 或命名返回值优化 (NRVO) 时,移动语义也会有所帮助。在这些情况下,如果类型定义了移动构造函数,编译器会调用它。

    为了更好地理解移动语义,请考虑将元素插入vector对象的示例。如果超出vector对象的容量,vector对象必须为其元素重新分配足够的内存,然后将每个元素复制到另一个内存位置,为插入的元素腾出空间。当插入操作复制元素时,它首先创建一个新元素。然后,它调用复制构造函数将数据从前一个元素复制到新元素。最后,它销毁前一个元素。移动语义使您能够直接移动对象,而无需进行昂贵的内存分配和复制操作。

    为了利用向量示例中的移动语义,您可以编写移动构造函数将数据从一个对象移动到另一个对象。

4.  完备转交(Perfect forwarding)

         完备转交减少了对重载函数的需求,并有助于避免转交问题。当你编写一个以引用为参数的泛型函数时,可能会发生转交问题。如果它将这些参数传递(或转交)给另一个函数,例如,如果它采用 const T& 类型的参数,则被调用的函数无法修改该参数的值。如果泛型函数采用 T& 类型的参数,则无法使用右值(如临时对象或整数文字)调用该函数

    考虑以下声明四种类型 W、X、Y 和 Z 的示例。每种类型的构造函数都采用不同的 const 和非 const 左值引用组合作为其参数。

struct W

{

    W(int&, int&) {}

};

struct X

{

    X(const int&, int&) {}

};

struct Y

{

    Y(int&, const int&) {}

};

struct Z

{

    Z(const int&, const int&) {}

};

假设您要编写一个生成对象的通用函数。以下示例显示了编写此函数的一种方法:

template <typename T, typename A1, typename A2>

T* factory(A1& a1, A2& a2)

{

    return new T(a1, a2);

}

以下示例显示了对函数factory的有效调用:

int a = 4, b = 5;

W* pw = factory<W>(a, b);

但是,以下示例不包含对 factory 函数的有效调用这是因为 factory 采用可修改的左值引用作为其参数,但它是使用右值进行调用的(译注:在C++11以前的版本中,传入右值的情况按左值引用方式处理,也就是说,如果按右值调用则更优):

Z* pz = factory<Z>(2, 2);

    通常,为了解决这个问题,您必须为 A& 和 const A& 参数的每种组合创建一个factory函数的重载版本。右值引用使您能够编写一个factory函数版本,如以下示例所示:

template <typename T, typename A1, typename A2>

T* factory(A1&& a1, A2&& a2)

{

    return new T(std::forward<A1>(a1), std::forward<A2>(a2));

}

此示例使用右值引用作为factory函数的参数。std::forward 函数的目的是将factory函数的参数转交给模板类的构造函数。

    以下示例显示了使用修改后的factory函数创建 W、X、Y 和 Z 类实例的主函数。修改后的factory函数将其参数(左值或右值)转交给相应的类构造函数。

    int main()

{

  int a = 4, b = 5;

  W* pw = factory<W>(a, b);

  X* px = factory<X>(2, b);

  Y* py = factory<Y>(a, 2);

  Z* pz = factory<Z>(2, 2);

  delete pw;

  delete px;

  delete py;

  delete pz;

}

5.  右值引用之属性

    您可以重载函数来获取左值引用和右值引用。

    通过重载函数以接受 const 左值引用或右值引用,您可以编写区分不可修改对象(左值)和可修改临时对象的值(右值之一)的代码。您可以将对象传递给接受右值引用的函数(除非该对象被标记为 const)。以下展示了一个函数f,其中重载了函数以接受左值引用和右值引用。主函数分别使用左值引用和右值引用调用函数 f。

// reference-overload.cpp

// Compile with: /EHsc

#include <iostream>

using namespace std;

// A class that contains a memory resource.

class MemoryBlock

{

    // TODO: Add resources for the class here.

};

void f(const MemoryBlock&)

{

    cout << "In f(const MemoryBlock&). This version can't modify the parameter." << endl;

}

void f(MemoryBlock&&)

{

    cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;

}

int main()

{

    MemoryBlock block;

    f(block);

    f(MemoryBlock()); //临时对象是右值之一

}

这个例子输出如下:

In f(const MemoryBlock&). This version can't modify the parameter.

In f(MemoryBlock&&). This version can modify the parameter.

    在此示例中,第一次调用 f 时将局部变量(左值)作为其参数传递。第二次调用 f 时将临时对象作为其参数传递。由于临时对象无法在程序中的其他地方引用,因此调用绑定到采用右值引用的 f 的重载版本,该版本可以自由修改对象。

    编译器将命名的右值引用视为左值,将未命名的右值引用视为右值。

    将右值引用作为参数的函数会将参数视为函数主体中的左值。编译器将命名的右值引用视为左值。这是因为命名对象可以被程序的多个部分引用。允许程序的多个部分修改或删除该对象的资源是危险的。例如,如果程序的多个部分尝试从同一对象转交资源,则只有第一次转交会成功。

    以下展示了一个函数 g ,其中重载函数以接受左值引用和右值引用。函数 f 将右值引用作为其参数(命名的右值引用)并返回右值引用(未命名的右值引用)。在从 f 调用 g 时,重载解析选择接受左值引用的 g 版本,因为 f 的主体将其参数视为左值。在从 main 调用 g 时,重载解析选择接受右值引用的 g 版本,因为 f 返回右值引用。

// named-reference.cpp

// Compile with: /EHsc

#include <iostream>

using namespace std;

// A class that contains a memory resource.

class MemoryBlock

{

    // TODO: Add resources for the class here.

};

void g(const MemoryBlock&)

{

    cout << "In g(const MemoryBlock&)." << endl;

}

void g(MemoryBlock&&)

{

    cout << "In g(MemoryBlock&&)." << endl;

}

MemoryBlock&& f(MemoryBlock&& block)

{

    g(block);

    return move(block);

}

int main()

{

    g(f(MemoryBlock()));

}

输出如下:

In g(const MemoryBlock&).

In g(MemoryBlock&&).

在该示例中,main 函数将一个右值传递给 f。f 的主体将其命名参数视为左值。从 f 到 g 的调用将参数绑定到左值引用(g 的第一个重载版本)。

    你可以将左值引用转换为右值引用。

C++ 标准库 std::move 函数可用于将对象转换为该对象的右值引用您还可以使用 static_cast 关键字将左值转换为右值引用,如以下示例所示:

// cast-reference.cpp

// Compile with: /EHsc

#include <iostream>

using namespace std;

// A class that contains a memory resource.

class MemoryBlock

{

    // TODO: Add resources for the class here.

};

void g(const MemoryBlock&)

{

    cout << "In g(const MemoryBlock&)." << endl;

}

void g(MemoryBlock&&)

{

    cout << "In g(MemoryBlock&&)." << endl;

}

int main()

{

    MemoryBlock block;

    g(block);

    g(static_cast<MemoryBlock&&>(block));

}

此示例产生以下输出:

In g(const MemoryBlock&).

In g(MemoryBlock&&).

    函数模板推断其模板参数类型,然后使用引用折叠规则。

    函数模板将其参数传递(或转发)给另一个函数是一种常见模式。了解模板类型推导如何适用于采用右值引用的函数模板非常重要。

    如果函数参数是右值,编译器会推断该参数为右值引用。例如,假设您将一个对类型 X 的对象的右值引用传递给以类型 T&& 作为参数的函数模板。模板参数推导会推断 T 为 X,因此该参数的类型为 X&&。如果函数参数是左值或 const 左值,编译器会推断其类型为该类型的左值引用或 const 左值引用。

    以下示例声明一个结构模板,然后将其特化为各种引用类型。print_type_and_value 函数以右值引用作为其参数,并将其转交给 S::print 方法的适当专用版本。main 函数演示了调用 S::print 方法的各种方式。

// template-type-deduction.cpp

// Compile with: /EHsc

#include <iostream>

#include <string>

using namespace std;

template<typename T> struct S;

// The following structures specialize S by

// lvalue reference (T&), const lvalue reference (const T&),

// rvalue reference (T&&), and const rvalue reference (const T&&).

// Each structure provides a print method that prints the type of

// the structure and its parameter.

template<typename T> struct S<T&> {

    static void print(T& t)

    {

        cout << "print<T&>: " << t << endl;

    }

};

template<typename T> struct S<const T&> {

    static void print(const T& t)

    {

        cout << "print<const T&>: " << t << endl;

    }

};

template<typename T> struct S<T&&> {

    static void print(T&& t)

    {

        cout << "print<T&&>: " << t << endl;

    }

};

template<typename T> struct S<const T&&> {

    static void print(const T&& t)

    {

        cout << "print<const T&&>: " << t << endl;

    }

};

// This function forwards its parameter to a specialized

// version of the S type.

template <typename T> void print_type_and_value(T&& t)

{

    S<T&&>::print(std::forward<T>(t));

}

// This function returns the constant string "fourth".

const string fourth() { return string("fourth"); }

int main()

{

    // The following call resolves to:

    // print_type_and_value<string&>(string& && t)

    // Which collapses to:

    // print_type_and_value<string&>(string& t)

    string s1("first");

    print_type_and_value(s1);

    // The following call resolves to:

    // print_type_and_value<const string&>(const string& && t)

    // Which collapses to:

    // print_type_and_value<const string&>(const string& t)

    const string s2("second");

    print_type_and_value(s2);

    // The following call resolves to:

    // print_type_and_value<string&&>(string&& t)

    print_type_and_value(string("third"));

    // The following call resolves to:

    // print_type_and_value<const string&&>(const string&& t)

    print_type_and_value(fourth());

}

输出如下:

print<T&>: first

print<const T&>: second

print<T&&>: third

print<const T&&>: fourth

为了解析对 print_type_and_value 函数的每个调用,编译器首先进行模板参数推导。然后,当编译器用推导的模板参数替换参数类型时,它会应用引用折叠规则。例如,将局部变量 s1 传递给 print_type_and_value 函数会导致编译器生成以下函数签名:

print_type_and_value<string&>(string&&& t)

编译器使用引用折叠规则来减少签名:

print_type_and_value<string&>(string& t)

然后,此版本的 print_type_and_value 函数将其参数转发给 S::print 方法的正确专用版本。

    下表总结了模板参数类型推导的引用折叠规则:

扩展类型

折叠类型

T& &

T&

T& &&

T&

T&& &

T&

T&& &&

T&&

模板参数推导是实现完备转发的重要元素。

6.  总结

右值引用将左值与右值区分开来。为了提高应用程序的性能,它们可以消除不必要的内存分配和复制操作。它们还使您能够编写一个接受任意参数的函数。该函数可以将它们转交给另一个函数,就像直接调用另一个函数一样。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com