Last Updated: 2023-08-20 14:15:50 Sunday
-- TOC --
本文尝试总结C++编译期模板类型推导的各种规则。
template<typename T>
void f(T ¶m);
int a {};
const int ca {a};
const int &ra {a};
f(a); // param's type is int&, T is int.
f(ca); // param's type is const int&, T is const int.
f(ra); // param's type is const int&, T is const int.
template<typename T>
void f(const T ¶m);
int a {};
const int ca {a};
const int &ra {a};
f(a); // param's type is const int&, T is int.
f(ca); // param's type is const int&, T is int.
f(ra); // param's type is const int&, T is int.
template<typename T>
void f(T *param);
int a {};
const int *pa {&a};
f(&a); // param's type is int*, T is int.
f(pa); // param's type is const int*, T is const int.
template<typename T>
void f(const T *param);
int a {};
const int *pa {&a};
f(&a); // param's type is const int*, T is int.
f(pa); // param's type is const int*, T is int.
template<typename T>
void f(T &¶m); // cannot be like const T&&
int a {};
const int ca {a};
const int &ra {a};
f(a); // param's type is int&, T is int&.
f(ca); // param's type is const int&, T is const int&.
f(ra); // param's type is const int&, T is const int&.
f(8); // param's type is int&&, T is int.
这是唯一能够推导出T为lvalue reference的场景!
param's type有左有右,但param在函数内部,始终是个lvalue!
template<typename T>
void f(T param);
int a {};
const int ca {a};
const int &ra {a};
volatile int va {a};
f(a); // param's type is int, T is int.
f(ca); // param's type is int, T is int.
f(ra); // param's type is int, T is int.
f(va); // param's type is int, T is int.
虽然ca和ra都有const修饰,但T依然只是int,因为通过by-value(copy)传参,param就不需要const。volatile同理,也不需要。
指针通过by-value推导的情况:
template<typename T>
void f(T param);
const char *p1 { "test" };
const char* const p2 { "test2" };
f(p1); // param's type is const char*, T is const char*.
f(p2); // param's type is const char*, T is const char*.
直接修饰p2的const,被拿掉了。
数组是不同于指针的类型,但在接口传参或赋值时,数组有时会decay
成为指针。
const char name[] {"cs.pynote.net"}; // type: const char[13]
const char *pname { name }; // array decays to pointer
知识点:函数接口参数不会是数组,只可能是指针或数组的引用。因此,下面两种定义完全相同,都是指针:
void f(int param[]);
void f(int *param);
所以,当数组作为参数传入函数模板时,数组类型会decay成为pointer。
template<typename T>
void f(T param); // by-value
f(name); // T is const char*.
但数据可以有引用类型:
template<typename T>
void f(T ¶m); // reference
f(name); // param's type is const char (&)[13],
// T is const char[13].
当函数本身作为参数时,函数类型也会decay,规则与数组decay相同。
void f(int, double); // f's type is void(int,double)
template<typename T>
void g(T param); // by-value
template<typename T>
void h(T ¶m); // reference
g(f); // decayed to pointer,
// param's type and T are both void (*)(int,double)
h(f); // no decay
// param's type is void (&)(int,double)
// T is void(int,double)
引用是个别名,函数的引用,与函数指针并无实际差异。
以下代码片段,来自我的ringbb.hpp:
template<typename T>
template<size_t N>
void byte_ring_buffer<T>::push_back(const char (&a)[N]){ // char array
/**
* reference parameter does not decay!
* if define: char a[] {"abcd"};
* N is 5, the last char is '\0' for sure.
* if define: char b[] {'a','b','c','d'};
* N is only 4.
* if define: char c[] {'a','b','c','\0'};
* the last \0 will be write into ring buffer!!
**/
push_back(a, N);
}
N的值可以在编译期被推导出来,在模板中直接使用。
我在阅读C++书籍的时候,看到的这句话,通过前面规则的学习发现,的确是这样的。
以下是自己写的decay_t的代码:
// decay, decay_t
template<typename T>
class decay{
using t = remove_reference_t<T>;
public:
using type = conditional_t<is_array_v<t>,
add_pointer_t<remove_extent_t<t>>,
conditional_t<is_function_v<t>,
add_pointer_t<t>,
remove_cv_t<t>>>;
};
template<typename T>
using decay_t = decay<T>::type;
通过这段代码,可以清楚的看到,decay到底在做什么:
所谓reference parameter doesn't decay,说的是如下定义:
template<typename T>
void f(T ¶m);
任何对象的引用都保持不变,cv也不会被拿掉。
by-value推导时,cv肯定被decay掉!
auto类型推导,正如推导template parameter T。
auto a {8}; // a is int.
const auto ca {a}; // ca is const int.
const auto &ra {a}; // ra is const int&.
auto b {ra}; // b is int
auto &&uref1 {a}; // a is int and lvalue,
// uref1 is int&.
auto &&uref2 {ca}; // ca is const int and lvalue,
// uref2 is const int&.
auto &&uref3 {88}; // int and rvalue,
// uref3's type is int&&,
// but uref3 itself is lvalue.
const char name[] {"cs.pynote.net"}; // type: const char[13]
const char *pname { name }; // array decays to pointer
auto arr1 { name }; // arr1 is const char*, decayed.
auto &arr2 { name }; // arr2 is const char (&)[13], no decay.
void f(int, double); // f's type is void(int,double)
auto f1 {f}; // f1 is void (*)(int,double)
auto &f2 {f}; // f2 is void (&)(int,double)
"Note that an initialization of type auto always decays!"
这是auto类型推导的一个例外,当auto与{}
一起出现的时候,auto推导出来的类型为std::initializer_list<T>
。
auto a {99}; // std::initializer_list<int> with value 99
auto b = {99}; // ditto
auto c {1,2,3.0}; // error! can not deduce T for initializer_list
c的错误,是因为推导std::initializer_list模板类型时,其内部元素的类型不同导致的。
这是auto类型推导与模板T类型推导唯一的不同之处!auto自动认为{}
为一个std::initializer_list<T>
类型,一般模板类型推导没有这个前提。
template<tyename T>
void f(std::initializer_list<T> param);
f({1,2,3,4,5});
f的申明,必须要明确说明是std::initializer_list<T>
。
从C++14开始,可以用auto来自动推导函数接口的返回值类型(包括lambda)。规则与template type deduction一致,因此,就与auto推导变量类型有所不同,这个不同就是std::initializer_list<T>
,这种类型不能够自动推导。
argument表示调用入参,parameter表示申明的参数。
类型推导对default argument不起作用。
template<typename T>
void f(T="");
f(1); // T is int
f(); // error!
修正方法之一,就是给T也来一个默认类型:
template<typename T=std::string>
void f(T="");
CTAD是C++17才有的feature!在此之前,即便是使用class template的default type,也需要写上<>
符号。
// before C++17
ring_buffer<> rb; // use default type
ring_buffer<std::byte> rb;
现在:
// after C++14
ring_buffer rb; // use default type
vector v { 1,2,3 }; // deduct type to vector<int>
只有在目标类型的构造函数使用模板参数的情况下,推导过程才有效,否则,就要使用配套的deduction guide!
本文链接:https://cs.pynote.net/sf/c/cpp/202302061/
-- EOF --
-- MORE --