Effective Modern C++之Item1

本文探讨了C++模板类型推导的三个主要场景,并通过八个实例详细解析了每种情况下的类型推导规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Item1 理解模板类型推导

      在Effective Modern C++一书中,条目1介绍了模板类型推导的一些基本规则,我现在对Item1 的内容进行一些翻译和总结。本文通过8个例子针对模板类型推导中的3种情况进行分别讨论。

      如果我们忽略一些细节,则我们大体上可认为模板如下所示:

template<typename T>
void f(ParamType param)

调用格式如下所示:

f(expr)
在编译期间,编译器通常会进行两方面的类型推导,第一方面是对T进行类型推导,第二是对ParamType进行类型推导。这两个类型通常是不一样的,ParamType可同包含一些其他修饰(比如const)。例如下面的模板声明:

Template<typename T>
void f(const T& param) //此例中,ParamType的类型为const T&

在模板类型推导中,需要考虑以下3中情况:

case 1: ParamType是指针或者引用,但它不是一个综合引用(Universal Reference)

            在这种情形下,类型按如下规则进行推导:

            1) 如果expr是引用类型,则忽略引用(&)部分

            2) 根据expr类型和ParamTypeType类型决定T的类型。(对这条规则我不太理解,我感觉应该是先对T进行类型推导,然后方可决定ParamType的类型)

例子 1:

 模板定义如下所示:    

 

template<typename T>

void f(T& param) // param 是一个引用类型

        现在我们有如下变量定义:

int x = 27 ;
const int cx = x ;
const int& rx = x ;

        则我们对T和param的类型推导结果如下所示:

f(x);     // T is int, param's type is int&
f(cx);    // T is const int,
          // param's type is const int&
f(rx);    // T is const int,
          // param's type is const int&

       在第二、三种情况中,cx、rx都为const类型,所以在对T进行类型推导时,T的类型继续保持const类型,编译器进行这样的推导是必须而且合理的,试想如果T的类型推导中不保持const属性,那在函数内部将有可能对这个变量进行某些修改,这些修改是被编译器所允许的。那将一个const(只读)类型的变量传进函数将是不安全的。

       在第三种情况中rx是个引用类型,根据情形1的规则,引用部分(&)将被忽略,故T的类型被推导为const int。如果T被推导为const int&,则param的类型就会被推导为const int& & , 意为引用之引用,这在C++中是不被允许的。为了避免出现这种情况(param被推导为“引用之引用”),解决办法是忽略掉“&”而将T推导为const int。

例子2:

       在例2中,我们将函数f形参类型有T&改为const T&, 如下所示:

template<typename T>
void f(const T& param);  // param is now a ref-to-const
int x = 27;              // as before
const int cx = x;        // as before
const int& rx = x;       // as before
f(x);                    // T is int, param's type is const int&
f(cx);                   // T is int, param's type is const int&
f(rx);                   // T is int, param's type is const int&

        在例2 的第二、第三种情况中,cx、rx是const类型,由于我们假设param是一个referenc-to-const类型,所以在对T进行类型推导的时候没有必要保持const修饰了。

例子3:param是指针(pointer)或者指向常量的指针(pointer-to-const)

template<typename T>
void f(T* param);   // param is now a pointer
int x = 27;         // as before
const int *px = &x; // px is a ptr to a read-only view of x

f(&x);              // T is int, param's type is int*
f(px);              // T is const int,
                    // param's type is const int*

       从例3中,我们可以看到,指针和引用的类型推导规则是类似的,当expr是指针类型时,对T的类型推导将忽略expr的指针修饰符(*),当param中包含常量(const)的时候,对T、param的类型推导中,将继续保留const关键字。


case 2: ParamType是一个综合引用(Universal Reference)

       当ParamType是一个综合引用(即“T&&”)时,类型推导遵循以下准则:

       1)如果expr是一个左值(lvalue),则对T以及ParamType的推导中,仍将他们推导为左值。

       2)如果expr是一个右值(rvalue),则按通常的规则对T以及ParamType进行推导。

例子 4:

template<typename T>
void f(T&& param);    // param is now a universal reference
int x = 27;           // as before
const int cx = x;     // as before
const int& rx = x;    // as before
f(x);                 // x is lvalue, so T is int&,
                      // param's type is also int&
f(cx);                // cx is lvalue, so T is const int&,
                      // param's type is also const int&
f(rx);                // rx is lvalue, so T is const int&,
                      // param's type is also const int&
f(27);                // 27 is rvalue, so T is int,
                      // param's type is therefore int&&

       从例子4中,我们可以看到,如果expr是个左值,则不论expr是否是引用类型,T以及ParamType都会被推导为引用类型,如果expr是const类型,则对T以及ParamType的推导都会继续保留const修饰符。综合引用的好处是可以实现完美转化(perfect forwarding)。”Perfect Forwarding”也被翻译成完美转发,精准转发等,说的都是一个意思。

精确传递适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:

左值/右值和 const/non-const。 精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。例如:

函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value。

forward_value 的定义为:

 template <typename T> void forward_value(const T& val) { 
  process_value(val); 
 } 
 template <typename T> void forward_value(T& val) { 
  process_value(val); 
 }

函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&,否则,下面四种不同类型参数的调用中就不能同时满足  :

  int a = 0; 
  const int &b = 1; 
  forward_value(a); // int& 
  forward_value(b); // const int& 
 forward_value(2); // int&

对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。我们看看右值引用如何帮助我们解决这个问题  :

 template <typename T> void forward_value(T&& val) { 
  process_value(val); 
 }

只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。四种不用类型参数的调用都能满足,参数的左右值属性和 const/non-cosnt 属性完全传递给目标函数 process_value。这个解决方案不是简洁优雅吗?

  int a = 0; 
  const int &b = 1; 
  forward_value(a); // int& 
  forward_value(b); // const int& 
  forward_value(2); // int&&


在对T&&的推导中,右值实参为右值引用,左值实参仍然为左值引用。一句话,就是参数的属性不变。这样也就完美的实现了参数的完整传递。

case 3: ParamType既不是指针(pointer)也不是引用(reference)

        当ParamType既不是指针也不是引用是,我们按值传递(pass-by-value)来处理。

template<typename T>
void f(T param);     // param is now passed by value

       也就是说在这种情况下,param是传递给函数的实参的拷贝,而不论实参是什么类型,具体来说:

       1) 如果expr是个引用类型,则T的推导中将忽略引用类型

       2) 如果expr是const类型,则忽略const类型,如果expr是volatile类型,仍然忽略掉

下面看一个例子:

例子 5

int x = 27;          // as before
const int cx = x;    // as before
const int& rx = x;   // as before
f(x);                // T and param are both int
f(cx);               // T and param are again both int
f(rx);               // T and param are still both int

在例5的三种情况中,对T、Param的推导中,去除了实参中的const、&等修饰,传进函数f的实参只是x、cx、rx的一个副本。

例子 6 expr是const-pointer-to-const-object

template<typename T>
void f(T param);                              // param is still passed by value
const char* const ptr = "Fun with pointers" ; // ptr is const pointer to const object

f(ptr);                                       // pass arg of type const char * const, T is const char * 

此时,传到函数中的指针只是ptr的一个副本,所以这个副本的指针的const属性在推导过程中被忽略了,但副本锁指向的内容仍然保留为const类型,故T的推导类型为const char *。

例子 7  数组类型的参数

template<typename T>
void f(T param);                 // template with by-value parameter
const char name[]="J. P. Briggs" ;
f(name);                         // what types are deduced for T and param?


       数组作为参数传递到函数中时,编译器会把他作为指针来处理,这时候数组会退化为指针类型。所以任何传递到此函数的数组都会被推导为指针,也就是说此时,T被推导为const char*。

       那么问题来了,如果我们想让编译器将T推导为数组类型的话,应该怎么改写模板呢?来看下面的模板声明:

template<typename T>
void f(T& param); // template with by-reference parameter
//and we pass an array to it,
f(name); // pass array to f

这时候,T就被推导为实际的数组类型了!并且这个推导包含数组的大小信息,即T被推导为const char[13]。

例子8 参数是函数

当我们想函数中传递实参的时候,数组类型的实参会退化为指针,具由这种特性的并非只有数组,函数也是其中一员。当实参是函数名字的时候,编译器会将其退化成函数指针。请看下面的例子

void someFunc(int, double); // someFunc is a function;
                            // type is void(int, double)
template<typename T>
void f1(T param);           // in f1, param passed by value
template<typename T>
void f2(T& param);          // in f2, param passed by ref
f1(someFunc);               // param deduced as ptr-to-func;
                            // type is void (*)(int, double)
f2(someFunc);               // param deduced as ref-to-func;
                            // type is void (&)(int, double)

      从例8中,我们看到,不管是数组还是函数,当通过“pass-by-value”类型的模板时,都会退化为指针类型,如果想让编译器将T推导为数组或者函数的实际类型,则应该用“T&”类型作为模板函数的形参。

      




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值