自学内容网 自学内容网

C++ 模板进阶知识——类型推断

C++ 模板进阶知识——类型推断

类型推断(推导)这件事情在模板编程中经常出现,很多时候编译器都会做类型推断。这方面的相关知识在现代C++语言开发中是经常用到的,算是一个比较大的知识点,有必要对类型推断的知识理论做一些储备。所以,本节主要讲解函数模板类型推断的知识。

1. 如何查看类型推断结果

要在C++中使用Boost库来获取和打印更人类可读的类型名称,首先需要确保已经安装了Boost库。Boost提供了一个名为boost::typeindex::type_id_with_cvr<T>()的功能,它可以返回一个表示类型的对象,该对象可以被用来获取类型的字符串表示。

以下是使用Boost TypeIndex库来打印模板参数T的类型:

#include <iostream>
#include <boost/type_index.hpp>

template<typename T>
void myfunc(T&& param) 
{
    // 使用Boost TypeIndex库来获取并打印类型
    std::cout << "T is: " 
              << boost::typeindex::type_id_with_cvr<T>().pretty_name() 
              << std::endl;
    std::cout << "param is: " 
              << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() 
              << std::endl;
}

int main()
{
    int x = 10;
    const int cx = x;
    const int& rx = x;

    myfunc(x);  // 应该显示 int
    myfunc(cx); // 应该显示 int const
    myfunc(rx); // 应该显示 int const&
    myfunc(42); // 应该显示 int
}

这里的关键改变是使用boost::typeindex::type_id_with_cvr<T>().pretty_name()来获取类型T的字符串表示。pretty_name()函数提供了一个比标准C++ typeid().name()更易于阅读的类型名称。

使用Boost库步骤

  1. 安装Boost:确保的系统上已经安装了Boost库。可以从Boost官方网站下载并按照指导进行安装。
  2. 配置项目:在你的编译器设置中包含Boost头文件的路径,并且如果使用到了需要链接的库,也要设置相应的库路径。
  3. 包含头文件:在源代码中包含必要的Boost头文件,如上例中的#include <boost/type_index.hpp>

注意

  • Boost TypeIndex库不需要链接任何特定的Boost库,因为它是header-only的。
  • 输出的类型名称会因编译器而异,但通常比直接使用typeid().name()更加清晰易懂。

2. 理解函数模板类型推断

2.1 指针或引用类型

在C++中,当使用函数模板时,编译器通过实参来自动推导模板参数T的类型。这个过程中,对于指针或引用类型的实参,有特定的规则需要了解。

2.1.1 忽略引用

当函数模板的实参是引用类型时,编译器在进行类型推导时会忽略掉引用部分。这意味着模板参数T不会包括引用属性。

示例代码:

#include <iostream>
#include <boost/type_index.hpp>

template<typename T>
void myfunc(T& param) {
    std::cout << "T is: " 
              << boost::typeindex::type_id_with_cvr<T>().pretty_name() 
              << std::endl;
    std::cout << "param is: " 
              << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() 
              << std::endl;
}

int x = 10;
int& k = x;
myfunc(k);  // T 被推导为 int,而不是 int&

此示例中,尽管k是对x的引用,但是在模板推断中,T只被推导为int

2.1.2 保持const属性

当传递给函数模板的引用类型形参带有const属性的实参时,这个const属性会影响到模板参数T的推导。

示例代码:

#include <iostream>
#include <boost/type_index.hpp>

template<typename T>
void myfunc(const T& param) {
    std::cout << "T is: " 
              << boost::typeindex::type_id_with_cvr<T>().pretty_name() 
              << std::endl;
    std::cout << "param is: " 
              << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() 
              << std::endl;
}

const int j = 20;
myfunc(j);  // T 被推导为 int, 形参 param 的类型为 const int&

在此示例中,jconst int类型,T被正确推导为int,而形参param的类型是const int&。这确保了j的常量性质不会被修改。

2.1.3 处理指针类型

对于指针类型的形参,类型推断也遵循特定的规则,尤其是在处理const修饰符时。

#include <iostream>
#include <boost/type_index.hpp>

template <typename T>
void myfunc(T* tmprv) 
{
    // 使用Boost TypeIndex库来获取并打印类型
    std::cout << "T is: " 
              << boost::typeindex::type_id_with_cvr<T>().pretty_name() 
              << std::endl;
    std::cout << "param is: " 
          << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name() 
          << std::endl;
}
int main() 
{
    int i = 18;
    const int* pi = &i;
    
    myfunc(&i);  // 查看实际执行结果:T=int, tmprv=int*
    myfunc(pi);  // 查看实际执行结果:T=int const, tmprv=int const*
}

在此示例中:

  • 当调用myfunc(&i)时,&i是一个指向int的指针,因此T被推导为int,而tmprv的类型为int*
  • 当调用myfunc(pi)时,pi是一个指向const int的指针,因此T被推导为int const,而tmprv的类型为int const*
2.1.4 结果说明
  • 忽略引用: 如果实参是引用类型,编译器在进行类型推导时会忽略引用的部分。这意味着即使实参是通过引用传递的,模板参数T也只代表被引用的对象的类型,而不包括引用本身。
  • 保持const属性: 如果实参带有const修饰符,这个const属性会影响模板参数T的推导。例如,如果传递给函数模板的是一个const对象的引用,T将被推导为不带const的类型,但函数参数类型将是const T&
  • 指针和const处理: 当形参是指针类型且不带const时,实参中的const属性会被保留在模板参数T中。这意味着如果传递了一个指向const类型的指针,T将被推导为const类型。然而,如果形参自身被声明为指向const的指针(如const T*),则无论实参是否带有constT都会被推导为基本类型。
  • 引用和const处理: 这种行为同样适用于形参为引用类型的情况,无论是T&还是const T&。在T&的情况下,T将被推导为实参的基本类型,而在const T&的情况下,即使实参是constT也只代表基本类型。

编码技巧:

  • 明确形参是否应包含const对于正确处理可能带有const属性的数据非常重要,尤其是在不希望修改数据或只读数据访问的场景中。
  • 使用指针或引用作为形参可以避免不必要的数据复制,同时也应注意正确处理const属性,以防意外修改数据。

2.2 万能引用类型

万能引用的神奇之处在于既能接受左值,又能接受右值,而且传入左值或右值作为实参时,表现不一样。

万能引用类型,也称为转发引用,是一种特殊的引用类型,在模板函数中通过T&&声明。C++的引用折叠规则使得万能引用可以根据传入的实参类型(左值或右值)来决定其最终类型:

  • 绑定到左值: 当一个左值被传递给万能引用时,万能引用将被推导为左值引用(T&)。这允许函数处理持久的对象,而不仅仅是临时值。

  • 绑定到右值: 当一个右值被传递给万能引用时,万能引用将被推导为右值引用(T&&)。这使得函数能够优化资源管理,例如通过移动语义避免不必要的复制。

万能引用因其能够同时接受左值和右值而广泛应用于泛型编程中。它们是实现高效、灵活代码的关键工具,特别是在模板编程和函数重载中非常有用。

示例代码:

#include <iostream>
#include <boost/type_index.hpp>

template<typename T>
void printType(T&& param) {
    std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
    std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name()
              << std::endl;
}

int main() {
    int x = 10;
    printType(x);  // 输出x为左值的类型信息
    printType(10); // 输出10为右值的类型信息
}

在这个示例中:

  • printType(x) 会打印出Tint&(因为x是一个左值)和param也为int&
  • printType(10) 会打印出Tint(因为10是一个右值)和paramint&&

2.3 传值方式

在C++中,当函数参数以传值方式接收时,无论原始对象是什么类型(包括指针、引用或常规变量),传递给函数的都是该对象的一个副本。这意味着在函数内部对这个副本的任何修改都不会影响到原始对象。

2.3.1 函数模板和参数推导

考虑以下函数模板定义:

#include <iostream>
#include <boost/type_index.hpp>

template <typename T>
void myfunc(T tmprv)
{
    std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
    std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name()
              << std::endl;
}

在主函数中,我们使用不同类型的变量来调用此模板函数:

int i = 18;                   // i 的类型为 int
const int j = i;              // j 的类型为 const int
const int& k = i;             // k 的类型为 const int &

myfunc(i);  // 实际执行结果:T=int, tmprv=int
myfunc(j);  // 实际执行结果:T=int, tmprv=int,const 属性没有传递,因为对方是新副本
myfunc(k);  // 实际执行结果:T=int, tmprv=int,const 属性和引用都没有传递,因为对方是新副本
结论
  1. 忽略引用性:若实参是引用类型,则引用部分会被忽略,T不会被推导为引用类型。手工指定引用类型(例如 myfunc<int &>(m);)不建议,以免出现意想不到的问题。

  2. 忽略顶层const:若实参是const类型,该const属性在类型推导时会被忽略,因为传递的是新副本。即使原始变量(如jk)不能修改,传递到函数模板中生成的新对象可以修改。

2.3.2 指针的情况
char mystr[] = "I Love China!";
const char* const p = mystr;

myfunc(p);  // 实际执行结果:T=const char*, tmprv=const char*

分析:

  • 第一个const(修饰指针所指向的内容)保留,表示tmprv指向的内容不能通过tmprv进行修改。
  • 第二个const(修饰指针本身)被忽略,表示tmprv可以重新指向其他地址。
myfunc()中测试指针行为
template <typename T>
void myfunc(T tmprv) {
    tmprv = nullptr;      // 可以执行,说明指针的常量性被忽略
    *tmprv = 'Y';         // 报错,“tmprv": 不能给常量赋值,说明指向内容的常量性被保留
}

这表明,即使传递的是const char*const char[]数组(constchar位置可互换),该const修饰的指向内容的常量性会被保留。

2.4 传值方式的引申—std::ref与std::cref

在C++函数模板中,默认情况下,参数是通过值传递的。这意味着传入函数的是参数的副本,而不是原始对象本身。这种机制保证了函数内部对参数的修改不会影响到原始数据。然而,对于大型对象或结构,这种方法可能会引起性能问题,因为创建副本需要额外的时间和内存。

2.4.1 std::ref 和 std::cref 介绍

为了解决这个问题,C++11提供了std::refstd::cref,它们定义在<functional>头文件中。这两个工具允许以引用的形式传递参数,而不是复制对象:

  • std::ref用于创建一个对象的引用。
  • std::cref用于创建一个对象的常量引用。

使用这些工具可以有效地减少不必要的数据复制,提高程序的运行效率。

2.4.2 示例与分析

考虑以下模板函数,它使用Boost库来打印类型信息,这有助于验证类型推导的行为:

#include <iostream>
#include <functional>
#include <boost/type_index.hpp>

template<typename T>
void myfunc(T tmprv) {
    std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
    std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name()
              << std::endl;
}

int main() {
    int x = 10;
    const int y = 20;

    // 传值方式
    myfunc(x);
    myfunc(y);

    // 使用 std::ref 和 std::cref
    myfunc(std::ref(x));
    myfunc(std::cref(y));

    return 0;
}

在此代码中:

  • myfunc(x)myfunc(y)通过值传递调用,T被推导为intconst int
  • myfunc(std::ref(x))myfunc(std::cref(y))通过引用传递调用,T被推导为int&const int&。这表明std::refstd::cref确实按预期工作,它们分别传递了引用和常量引用,从而避免了不必要的对象复制。

使用std::refstd::cref可以使函数模板更加灵活和高效,特别是在处理需要避免复制的大型数据结构时。这种方法不仅节省了资源,还保持了代码的简洁和易于理解。

2.5 数组作为实参

当数组被用作函数模板的实参时,其处理方式依赖于参数传递的方式:是通过值还是通过引用。

2.5.1 通过值传递

在C++中,当数组作为函数参数通过值传递时,它不会将整个数组的副本传递给函数,而是只传递一个指向数组首元素的指针,这种现象称为“数组退化”。

当数组通过值传递给函数模板时,数组名会退化成指向其首元素的指针。因此,模板参数被推导为指针类型:

template <typename T>
void myfunc(T tmprv) {
    std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
    std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name()
              << std::endl;
}

int main() {
    const char mystr[] = "I Love China!";
    myfunc(mystr); // 实际执行结果:T=const char*, tmprv=const char*
}

这里,mystr 被退化为 const char*,因此 Tconst char*

2.5.2 通过引用传递

修改函数模板使其通过引用接收参数可以保留数组的完整性,避免退化为指针:

template <typename T>
void myfunc(T& tmprv) {
    std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
    std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name()
              << std::endl;
}

int main() {
    const char mystr[] = "I Love China!";
    myfunc(mystr); // 实际执行结果:T=const char [14], tmprv=const char (&)[14]
}

在这种情况下,数组不会退化,T 被推导为具体的数组类型 const char [14],而 tmprv 是该数组的引用。

2.6 函数名作为实参

在C++中,函数名可以用作函数模板的实参,在编写需要回调函数的代码或实现高阶函数时,经常需要将函数名作为参数传递给其他函数。使用模板可以使这类函数更加通用和灵活。

2.6.1 通过值传递

当函数名作为实参传递时,默认被视为指向该函数的指针:

void testFunc() {}

template <typename T>
void myfunc(T tmprv) {
    std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
    std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name()
              << std::endl;
}

int main() {
    myfunc(testFunc); // 实际执行结果:T=void (*)(void), tmprv=void (*)(void)
}

这里,testFunc 被视为 void (*)(void) 类型,即指向无参、无返回值函数的指针。

2.6.2 通过引用传递

通过修改模板参数为引用类型,可以获取到函数的引用,而非指针:

template <typename T>
void myfunc(T& tmprv) {
    std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
    std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name()
              << std::endl;
}

int main() {
    myfunc(testFunc); // 实际执行结果:T=void (&)(void), tmprv=void (&)(void)
}

在这种情况下,Ttmprv 被推导为函数的引用类型 void (&)(void),这显示了C++对函数的引用支持。

2.7 初始化列表作为实参

在C++中,初始化列表(std::initializer_list)提供了一种方便的方法来处理未知数量的同类型参数。这在模板编程中尤其有用,因为它允许函数接受任意数量的同类型参数,而无需预先定义参数的数量。

注意:std::initializer_list通常用于处理同类型的数据集合。如果需要处理不同类型的数据,可能需要考虑使用其他容器或结构,如std::tuple自定义的结构体

2.7.1 包含必要的头文件

要使用std::initializer_list,首先需要包含相应的头文件。通常,这会是<initializer_list>,它定义了std::initializer_list类。此外,为了进行输出等操作,可以包含<iostream>头文件:

#include <initializer_list>
#include <iostream>
2.7.2 定义一个接受初始化列表的模板函数

你可以定义一个模板函数,该函数接受一个类型为std::initializer_list<T>的参数。这允许你传递一个由花括号 {} 包围的元素列表给函数,如下所示:

#include <boost/type_index.hpp> // 需要包含 Boost TypeIndex 头文件

template <typename T>
void myfunc(std::initializer_list<T> tmprv) {
    for (const auto& item : tmprv) {
        std::cout << item << " ";
        std::cout << "T is: "
              << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << std::endl;
   std::cout << "param is: "
              << boost::typeindex::type_id_with_cvr<decltype(tmprv)>().pretty_name()
              << std::endl;
    }
    std::cout << std::endl;
}

在这个函数中,遍历初始化列表中的每个元素,并将其打印出来。通过打印出每个元素的类型和参数的类型,可以更深入地理解类型推断和模板的行为。使用范围基于的for循环(range-based for loop)使代码简洁且易于理解。

2.7.3 使用初始化列表调用函数

在主函数中,你可以通过以下方式调用myfunc,传入一个初始化列表:

int main() {
    myfunc({1, 2, 3, 4, 5}); // 调用模板函数并传入一个整数列表
    myfunc({"apple", "banana", "cherry"}); // 调用相同的模板函数并传入一个字符串列表
}

这样的调用方式表明,myfunc能够接受任何类型的元素列表,只要这些元素类型相同。每次调用时,模板参数T被推导为列表中元素的类型,无论是intstd::string还是其他任何类型。

2.8 类型推断总结

  1. 推断中,引用类型实参的引用属性会被忽略
    • 在类型推断过程中,如果实参是引用类型,其引用属性会被忽略。这意味着不论实参是左值引用还是右值引用,都会被视为其底层类型进行推断。
  2. 万能引用(通用引用)的推断依赖于实参是左值还是右值
    • 当模板参数按值传递时,实参的const属性不影响推断结果,因此const修饰符会被忽略。然而,如果传递的是指向const的指针或引用,其const属性仍然保留。
  3. 按值传递的实参,传递给形参时const属性不起作用,但如果传递的是指针,则保持其const属性
    • 当模板参数按值传递时,实参的const属性不影响推断结果,因此const修饰符会被忽略。然而,如果传递的是指向const的指针或引用,其const属性仍然保留。
  4. 数组或函数类型在类型推断中默认被视为指针,除非模板形参是引用类型
    • 在类型推断中,数组或函数名将退化为相应的指针类型,除非模板形参明确声明为引用类型,这时候不会发生退化。
  5. 初始化列表必须在函数模板的形参中明确使用std::initializer_list<T>才能正确推断
    • std::initializer_list类型无法自动从花括号初始化列表推断得出,必须在函数模板的形参中显式声明为std::initializer_list<T>类型。

3. auto类型常规推断

auto并不陌生,在C++98时代就有auto关键字(也可以叫作说明符),但因为这个时代的auto用处不大,所以到了C++11时代,auto被赋予了全新的含义—用于变量的自动类型推断。换句话说,就是在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型,不需要程序员显式地指定类型。

特点:

  1. 编译期推断auto的类型推断完全在编译期完成,确保了类型的确定性和优化。
  2. 初始化要求:声明auto类型的变量时必须立即初始化,以便编译器能推断出其类型。
  3. 灵活性auto可以与指针、引用、const等限定符一起使用,提供了高度的灵活性。
  4. 类型占位符:在类型推断完成后,auto实际上代表了一个具体的类型,相当于模板类型参数T

3.1 传值方式(非指针,非引用)

当使用auto进行变量声明时,默认情况下,auto采用传值方式,这意味着引用和顶层const限定符将被忽略。

const int x = 10;
auto a = x; // a 是 int 类型,忽略 const

3.3 指针或引用类型,但不是万能引用

通过在auto后加&,声明变量为引用类型。在这种情况下,auto不会忽略底层const

const int x = 10;
auto& b = x; // b 是 const int& 类型,保留 const

3.4 万能引用类型

auto&&可以根据初始化表达式是左值还是右值来推断为左值引用或右值引用,这与函数模板中的万能引用类似。

int x = 10;
const int x2 = 20;
auto&& wnyy0 = 222;  // 222 是右值,wnyy0 推断为 int&&
auto&& wnyy1 = x;    // x 是左值,wnyy0 推断为 int&
auto&& wnyy2 = x2;   // x2 是左值且为 const,wnyy2 推断为 const int&

4. 完美转发

完美转发是C++中一种高级的技术,用于在函数模板中转发参数至另一个函数,同时保持所有参数的值类别(左值、右值)和其他属性(如const修饰符)不变。这一技术主要通过模板和std::forward实现,并在泛型编程中尤为重要,因为它允许函数模板在不丢失任何参数信息的前提下传递参数。

4.1 完美转发的步骤演绎

  1. 理解直接调用与转发的区别
    • 直接调用:函数直接从其它函数如main()中被调用。
    • 转发:函数通过另一个函数(通常是模板函数)将参数传递给第三个函数。
  2. 识别完美转发的需求
    • 在多层函数调用中,特别是当设计到模板函数作为"跳板"(即中间函数)时,保持参数的原始类型(包括其左值或右值特性)非常关键。
  3. 使用std::forward实现完美转发
    • std::forward是一个模板函数,它能够根据其模板参数的类型推断出传入参数的正确值类别。
    • 它只应在接受通用引用(形式为T&&,其中T是模板类型参数)的模板函数中使用。

示例说明:

考虑以下函数模板,它旨在将接收到的参数转发给另一个函数:

#include <iostream>

// 定义 funcLast 接收 int 参数的函数
void funcLast(int& x) {
    std::cout << "Received an lvalue: " << x << std::endl;
}

void funcLast(int&& x) {
    std::cout << "Received an rvalue: " << x << std::endl;
}

template<typename T>
void funcMiddle(T&& param)
{
    // 使用 std::forward 确保 param 保持其原始类型特征
    funcLast(std::forward<T>(param));
}

int main() {
    int a = 10;
    funcMiddle(a);  // 应输出: Received an lvalue: 10
    funcMiddle(20); // 应输出: Received an rvalue: 20
}

在这个例子中:

  • funcMiddle 使用了转发引用 T&& 来接收任何类型的参数。
  • std::forward<T>(param) 确保参数 param 以其原始的值类别(左值或右值)传递给 funcLast
  • funcLast 有两个重载版本,一个接收左值引用,另一个接收右值引用。这样可以根据传入的参数类型进行相应的处理。

这种方式确保了不论是左值还是右值,都能被 funcMiddle 正确地转发,并由 funcLast 接收并处理。这显示了完美转发在保持参数属性不变的同时,有效地将参数从一个函数传递到另一个函数的能力。

完美转发的关键点
  • 正确使用std::forward:仅在模板函数中,并且配合通用引用参数使用。
  • 避免额外的拷贝和移动操作:完美转发可以避免在函数间传递时不必要的拷贝和移动操作,优化性能。
  • 维护参数的完整性:确保参数的const属性和值类别在转发过程中不被改变,这对于编写通用代码库和API尤为重要。

完美转发使得函数模板可以作为灵活且高效的"中间人",在不破坏参数原有特性的情况下,将参数从一个函数传递到另一个函数。

4.2 std::forward

std::forward是C++11引入的一个模板函数,主要用于实现参数的完美转发。它的核心作用是在模板函数中保持参数的原始值类别(左值或右值)。std::forward通常与通用引用(Universal References,形式为T&&)一起使用,这种引用可以绑定到左值或右值上。通过使用std::forward,可以确保在函数模板中转发参数时,保持其左值或右值属性不变。

4.2.1 工作原理

std::forward 根据传入参数的类型在编译时确定返回左值引用或右值引用:

  • 当传递给std::forward的参数是一个左值时,std::forward返回一个左值引用。
  • 当传递给std::forward的参数是一个右值时,它返回一个右值引用。

这种行为使得std::forward非常适合用于函数模板中,尤其是那些需要根据参数原始类型将参数转发到其他函数的模板。

示例代码:

#include <iostream>

void receive(int& x) {
    std::cout << "Lvalue received: " << x << std::endl;
}

void receive(int&& x) {
    std::cout << "Rvalue received: " << x << std::endl;
}

template<typename T>
void relay(T&& arg) {
    // 使用 std::forward 确保 arg 的值类别(左值或右值)被保持
    receive(std::forward<T>(arg));
}

int main() {
    int lvalue = 10;
    relay(lvalue);  // 输出: Lvalue received: 10
    relay(20);      // 输出: Rvalue received: 20
}

在此代码中:

  • relay(lvalue):由于 lvalue 是左值,std::forwardarg 作为左值传递给 receive
  • relay(20):字面量 20 是右值,因此 std::forwardarg 作为右值传递。
4.2.2 重要性

std::forward的使用是现代C++中编写高效且类型安全代码的关键工具之一。它允许开发者编写可接受任何类型参数的泛型函数,并确保这些参数以最优的方式被处理(避免不必要的拷贝或移动操作),同时保持参数的原始属性不变。

总结来说,std::forward是实现完美转发的必备工具,它确保了参数在转发过程中保持其原始的左值或右值特性,从而使得函数模板可以更加灵活和高效地处理各种调用场景。

4.3 普通参数的完美转发

使用std::forward可以确保普通参数在转发时保持其原始状态(左值或右值)。

#include <iostream>

// 定义 funcLast 接收左值和右值重载版本
void funcLast(int& x) {
    std::cout << "Lvalue received in funcLast: " << x << std::endl;
}

void funcLast(int&& x) {
    std::cout << "Rvalue received in funcLast: " << x << std::endl;
}

// funcMiddle 使用模板和 std::forward 完美转发参数
template<typename T>
void funcMiddle(T&& param) {
    funcLast(std::forward<T>(param));
}

int main() {
    int x = 10;
    funcMiddle(x);  // x 作为左值传递
    funcMiddle(20); // 20 作为右值传递
}

在这个示例中:

  • funcMiddle 接收一个通过通用引用传递的参数 param
  • 使用 std::forward<T>(param),该函数确保 param 的值类别(左值或右值)在调用 funcLast 时被保持。
  • 这样,无论是传递给 funcMiddle 的是左值还是右值,funcLast 都能接收到正确的值类别,并进行相应的处理。

4.4 在构造函数模板中使用完美转发范例

在类模板的构造函数中使用完美转发可以有效地将构造参数直接转发给成员变量或基类的构造函数。

#include <iostream>
#include <string>

template<typename T>
class MyClass {
public:
    T value;

    // 使用模板构造函数和完美转发来初始化成员变量
    template<typename U>
    MyClass(U&& val) : value(std::forward<U>(val)) {}

    void print() const {
        std::cout << "Value: " << value << std::endl;
    }
};

int main() {
    std::string str = "Hello, World!";
    MyClass<std::string> obj1(str); // 传递左值
    MyClass<std::string> obj2(std::move(str)); // 传递右值

    obj1.print(); // 输出: Value: Hello, World!
    obj2.print(); // 输出: Value: Hello, World!
}

在这个示例中:

  • obj1 通过传递一个左值字符串 str 构造。
  • obj2 则通过传递 str 的右值(使用 std::move)构造。
  • 在两种情况下,MyClass 的构造函数都使用 std::forward<U>(val) 确保 val 的值类别在初始化 value 成员时得以保持。

4.5 在可变参数模板中使用完美转发范例

4.5.1 常规的在可变参模板中使用完美转发

在 C++11 及以后的版本中,可变参数模板和完美转发一起使用,允许函数接收任意数量和类型的参数,并将它们无缝地转发到其他函数。这在编写包装器、委托或代理函数时特别有用。

#include <iostream>

template<typename Func, typename... Args>
void wrapper(Func&& f, Args&&... args) {
    f(std::forward<Args>(args)...);
}

void print(int a, double b, const std::string& c) {
    std::cout << "Int: " << a << ", Double: " << b << ", String: " << c << std::endl;
}

int main() {
    std::string str = "example";
    wrapper(print, 42, 3.14159, str);
}

在这个示例中:

  • wrapper 函数接收一个函数 f 和一系列参数 args
  • 使用 std::forward<Args>(args)... 完美转发所有参数到函数 f
  • 这种方式确保了所有参数的值类别和类型在传递过程中保持不变。
4.5.2 将目标函数中的返回值通过转发函数返回给调用者函数

当使用完美转发在可变参数模板中转发函数调用时,也可以保留被调用函数的返回值类型,并将其返回给原始调用者。

#include <iostream>
#include <utility> // 包含 std::forward

template<typename Func, typename... Args>
auto forwarder(Func&& f, Args&&... args) -> decltype(auto) {
    return f(std::forward<Args>(args)...);
}

int add(int x, int y) {
    return x + y;
}

int main() {
    int result = forwarder(add, 5, 3);
    std::cout << "Result of addition: " << result << std::endl;
}

在这个示例中:

  • forwarder 函数不仅转发参数,还转发了 add 函数的返回值类型。
  • 使用 decltype(auto) 自动推断返回类型,确保返回类型与 add 函数的返回类型完全一致。
  • 这种方法允许 forwarder 函数保持高度的灵活性和通用性,能够处理各种返回类型的函数。

4.6 完美转发失败的情形一例

在 C++ 中,完美转发旨在将参数无缝地传递给另一个函数,同时保持参数的类型和值类别。然而,存在一些特殊场景,其中完美转发可能无法按预期工作,导致编译错误或运行时行为不正确。一个典型的例子是尝试使用 0NULL 作为指针来进行完美转发。

问题描述:

在 C++中,0NULL 常被用作空指针常量。然而,在模板函数中使用完美转发时,这些值会被推断为整数类型而非指针类型。这会导致类型不匹配的问题,特别是在函数重载解析中。

示例代码详解:

#include <iostream>

void funcLast(int* ptr) {
    if (ptr) {
        std::cout << "Pointer is not null." << std::endl;
    } else {
        std::cout << "Pointer is null." << std::endl;
    }
}

template<typename T>
void funcMiddle_Temp(T&& arg) {
    funcLast(std::forward<T>(arg));
}

int main() {
    int* ptr = nullptr;
    funcMiddle_Temp(ptr);  // 正常工作,ptr 是 nullptr 类型

    funcMiddle_Temp(0);    // 编译错误,0 被推断为 int 而非 int* 类型
    funcMiddle_Temp(NULL); // 可能的编译错误,NULL 被推断为 int 而非 int* 类型
}

在这个示例中:

  • funcMiddle_Temp 接收 nullptr 时,一切正常,因为 nullptr 的类型是 nullptr_t,可以被正确推断并转发。
  • 然而,当传递 0NULL 时,由于它们可以被解释为整数,导致编译器无法将其推断为指针类型。这将引起编译错误,因为 funcLast 需要一个 int* 类型的参数。

解决方案

为了避免这种情况,建议使用 nullptr 来表示空指针,而不是 0NULL,因为 nullptr 在类型推断中表现更加明确和一致。

int main() {
    funcMiddle_Temp(nullptr); // 正确,nullptr 明确表示空指针
}

通过这个例子,可以看到在使用模板和完美转发时需要注意类型推断的问题。在设计接口和编写通用代码时,正确理解和应用类型安全的实践尤为重要,以避免潜在的错误和混淆。


原文地址:https://blog.csdn.net/qq_68194402/article/details/141966812

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!