C++编译期模板类型推导(Template Type Deduction)

Last Updated: 2023-08-20 14:15:50 Sunday

-- TOC --

本文尝试总结C++编译期模板类型推导的各种规则。

(1)当模板的参数类型为reference或pointer时,通过对入参类型进行模式匹配,来推导模板类型T。

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

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 &param);

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.

(2)当模板的参数类型为Universal Reference时,如果入参类型是lvalue,param's type and T are both lvalue reference。如果入参类型是rvalue,按规则(1)推导。

template<typename T>
void f(T &&param);  // 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!

(3)模板的参数类型是by-value类型。

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,被拿掉了。

(4)Array Argument

数组是不同于指针的类型,但在接口传参或赋值时,数组有时会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 &param);  // reference

f(name);  // param's type is const char (&)[13],
          // T is const char[13].

(5)Function Argument

当函数本身作为参数时,函数类型也会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 &param); // 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)

引用是个别名,函数的引用,与函数指针并无实际差异。

(6)推导数组size

以下代码片段,来自我的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的值可以在编译期被推导出来,在模板中直接使用。

(7)Reference Parameter Doesn't Decay

我在阅读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 &param);

任何对象的引用都保持不变,cv也不会被拿掉。

by-value推导时,cv肯定被decay掉!

(8)auto type deduction is template type deduction

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!"

(9)auto with Uniform Initialization

这是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>

(10)auto return type

从C++14开始,可以用auto来自动推导函数接口的返回值类型(包括lambda)。规则与template type deduction一致,因此,就与auto推导变量类型有所不同,这个不同就是std::initializer_list<T>,这种类型不能够自动推导。

(11)Default Argument

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="");

(12)Class Template Argument Deduction(CTAD)(C++17)

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