Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

c++11-17 模板核心知识(五)—— 理解模板参数推导规则 #155

Open
zhangyachen opened this issue Nov 15, 2020 · 0 comments
Labels

Comments

@zhangyachen
Copy link
Owner

zhangyachen commented Nov 15, 2020

首先我们定义一下本文通用的模板定义与调用:

template<typename T>
void f(ParamType param);

......
f(expr); // call f with some expression

在编译阶段使用expr来推断ParamTypeT这两个类型。这两个类型通常不同,因为ParamType会有const和引用等修饰。例如:

template<typename T>
void f(const T& param);      // ParamType is const T&
int x = 0;
f(x);      // call f with an int

这里,T被推断成int,但是ParamType的类型是const T&

直觉下T的类型应该和expr的一样,比如上面的例子中,exprT的类型都是int。但是会有一些例外情况:T的类型不仅依赖expr,还依赖ParamType。总共分为三大类:

  • ParamType是一个指针或者引用,但不是universal reference(或者叫forwarding references).
  • ParamType是一个universal reference
  • ParamType既不是指针也不是引用。

Case 1 : ParamType是一个指针或者引用,但不是universal reference

  • 如果expr是一个引用,忽略其引用部分。
  • 比较exprParamType的类型来决定T的类型。

T&

template<typename T>
void f(T& param);       // param is a reference

......
int x = 27;                  // x is an int
const int cx = x;       // cx is a const int
const int& rx = x;     // rx is a reference to x as a const int

// call f
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&

上面例子是左值引用,但是这点对右值引用也适用。
注意第三点,const修饰符依旧保留。 这和普通函数的类似调用有区别:

void f(int &x){

}

... 
const int x  = 10;
f(x);       // error

const T&

如果给ParamType加上const,情况也没有太大变化:

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&

T*

改为指针也一样:

template<typename T>
void f(T* param); // param is now a pointer

......
int x = 27;                    
const int *px = &x;      

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

Case 2 : ParamType是Universal Reference

  • 如果expr是左值,那么TParamType会被推断为左值引用。
  • 如果expr是右值,那么就是Case 1的情况。
template<typename T>
void f(T&& param);       // param is now a universal reference

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

调用:

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&&

如果之前了解过完美转发和折叠引用的概念,结合Case1,这一个规则还是比较好理解的。

注意区别Universal Reference与右值引用

这两点需要区分清楚,比如:

template<typename T>
void f(T&& param);           // universal reference


template<typename T>
void f(std::vector<T>&& param);       // rvalue reference

有一个通用规则 : universal reference会有类型推断的过程。具体在后面的单独文章会讲,跟这篇文章的主题关系不大,这里稍微提一下 : )

Case 3 : ParamType既不是指针也不是引用

这种情况就是pass-by-value的情况:

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

这意味着,param是一个被拷贝的全新对象,也就是param决定着T的类型:

  • 如果expr是引用类型,忽略。
  • 如果expr带有const、volatile,忽略。
int x = 27;
const int cx = x; 
const int& rx = x; 
f(x);         // T's and param's types are both int
f(cx);      // T's and param's types are again both int
f(rx);      // T's and param's types are still both int

忽略const和volatile也比较好理解:参数是值拷贝,所以形参和实参其实是互相独立的。正如下面代码可以将const int传递给int,但是声明为引用则不行:

void f(int x){

}

int main() {
  const int x  = 10;

  f(x);       
}

注意忽略的const是针对参数本身的,而不针对指针指向的const对象:

template<typename T>
void f(T param);

......
const char* const ptr = "Fun with pointers";       // ptr is const pointer to const object
f(ptr);             // pass arg of type const char * const

这个按照值传递的是ptr,所以ptr的const会被忽略,但是ptr指向的对象依然是const。

数组作为参数

数组类型和指针类型是两种类型,但是有时候他们是可以互换的,比如在下面这种情况下,数组会decay成指针:

const char name[] = "J. P. Briggs";     // name's type is const char[13]
const char * ptrToName = name;       // array decays to pointer

在普通函数中,函数形参为数组类型和指针类型是等价的:

void myFunc(int param[]);
void myFunc1(int* param);         // same function as above

但是数组作为模板参数是比较特殊的一种情况。

ParamType按值传递

template<typename T>
void f(T param); // template with by-value parameter

......
const char name[] = "J. P. Briggs";     // name's type is  const char[13]

f(name);           // name is array, but T deduced as const char*

这种情况下,T被推断为指针类型const char*.

ParamType为引用类型

template<typename T>
void f(T& param); 

......
const char name[] = "J. P. Briggs";     // name's type is  const char[13]
f(name);             // pass array to f

现在T被推断为数组类型const char [13]ParamTypeconst char (&)[13],这种情况是很特殊的,要与ParamType按值传递区分开。

我们可以利用上面这种特性定义一个模板来推断数组的大小,这种用法还蛮常见的:

template<typename T, std::size_t N> 
constexpr std::size_t arraySize(T (&)[N]) noexcept  {
    return N;
}

......
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
std::array<int, arraySize(keyVals)> mappedVals;

image

函数作为参数

上面讨论的关于数组的情况同样适用于函数作为参数,函数类型同样也可以decay成函数指针:

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)

不过这在平时应用中也没有太大差别。

(完)

朋友们可以关注下我的公众号,获得最及时的更新:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant