Ch3nyang's blog

home

home

person

about

hive

project

collections_bookmark

mindclip

rss_feed

rss

类型推导

calendar_month 2022-01
archive 编程
tag cpp tag type

C++11/14/17 学习的第二篇:模板类型推导、autodecltype

模板类型推导

函数模板通常写法如下:

template<typename T>
void f(ParamType param); // ParaType 为含有 T 的类型表达式

调用这个模板:

f(expr);

在编译的时候,编译器通过 expr 来进行推导出两个类型:一个是 T 的,另一个是ParamType 。这两个类型不一定相同。比如:

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

如果有这样的调用:

int x = 0
f(x)

T 被推导成 int,而 ParamType 被推导成 const int&

  • ParamType 为引用或者指针时

    1. 如果 expr 的类型是个引用,忽略引用的部分
    2. 然后利用 expr 的类型和 ParamType 对比去判断 T 的类型。

    下面是一个例子:

    template<typename T>
    void f(T& param);
      
    int a = 1; // a 为 int
    const int b = a; // b 为 const int
    const int& c = a; // c 为 const int 的引用
      
    f(a); // T 为 int,param 为 int&
    f(b); // T 为 const int,param 为 const int&
    f(c); // T 为 const int,param 为 const int&
    

    左值引用和右值引用在此处没有区别。

    T& 变成 const T&,则原来的 const 会被扔掉。下面是一个例子:

    template<typename T>
    void f(const T& param);
      
    int a = 1; // a 为 int
    const int b = a; // b 为 const int
    const int& c = a; // c 为 const int 的引用
      
    f(a); // T 为 int,param 为 const int&
    f(b); // T 为 int,param 为 const int&
    f(c); // T 为 int,param 为 const int&
    

    param 是一个指针,推导方法基本相同。下面是一个例子:

    template<typename T>
    void f(T* param);
      
    int a = 1; // a 为 int
    const int *b = &a; // b 为指向 const int a 的指针
      
    f(&a); // T 为 int,param 为 int*
    f(b); // T 为 const int,param 为 const int*
    
  • ParamType 为万能引用时

    1. 如果 expr 是一个左值, TParamType 都会被推导成左值引用
    2. 如果 expr 是一个右值,那么就会按照前一种情况正常推导

    下面是一个例子:

    template<typename T>
    void f(T&& param);
      
    int a = 1; // a 为 int
    const int b = a; // b 为 const int
    const int& c = a; // c 为 const int 的引用
      
    f(a); // a 为左值,故 T 为 int&,param 为 int&
    f(b); // b 为左值,故 T 为 const int&,param 为 const int&
    f(c); // c 为左值,故 T 为 const int&,param 为 const int&
    f(1); // 1 为右值,故 T 为 int,param 为 int&&
    

    这里运用了引用折叠,具体可以参见我上一篇文章。

  • ParamType 为普通类型时

    1. 此时直接当作值传递。
    2. 忽略掉所有的引用、constvolatile

    下面是一个例子:

    template<typename T>
    void f(T param);
      
    int a = 1; // a 为 int
    const int b = a; // b 为 const int
    const int& c = a; // c 为 const int 的引用
      
    f(a); // T 为 int,param 为 int
    f(b); // T 为 int,param 为 int
    f(c); // T 为 int,param 为 int
    

    这相当于在传参时,对原来的变量做了一份拷贝。

  • 传递数组时

    数组可以退化为指针。

    下面是一个例子:

    template<typename T>
    void f(T param);
      
    const char a[] = "HelloWorld";
      
    f(a); // T 为 const char*
    

    然而,数组和指针并不完全相同。当 ParamType 为引用时,会推导出带有大小的数组。

    下面是一个例子:

    template<typename T>
    void f(T& param);
      
    const char a[] = "HelloWorld";
      
    f(a); // T 为 const char[10],param 为 const char(&)[10]
    
  • 传递函数时

    类似的,函数也会退化为指针。

    下面是一个例子:

    template<typename T>
    void f1(T param);
    template<typename T>
    void f2(T& param);
      
    void a(int double);
      
    f1(a); // param 为 void(*)(int, double)
    f2(a); // param 为 void(&)(int, double)
    

根据以上内容,我们可以总结:

  • 引用会被忽略
  • 万能引用中,左值会被特殊处理
  • 按值传递时,cv 特性会被忽略
  • 数组和函数会被退化为指针,除非是用在引用类型

auto 类型推导

auto 类型推导就是模板类型推导。

下面是几个例子:

const char g[] = "HelloWorld";
void j(int, double);

auto a = 1; // a 为 int
const auto b = a; // b 为 const int
const auto& c = a; // c 为 const int&
auto&& d = a; // a 为左值,故 d 为 int&
auto&& e = b; // b 为左值,故 e 为 const int&
auto&& f = 1; // 1 为右值,故 f 为 int&&
auto h = g; // h 为 const char*
auto& i = g; // i 为 const char(&)[10]
auto k = j; // k 为 void (*)(int, double)
auto& k = j; // k 为 void (&)(int, double)

但是,在统一初始化时,auto 推导会有所不同。

下面是一个例子:

auto a = 1; // a 为 int,值为 1
auto b(1); // b 为 int,值为 1
auto c = { 1 }; // c 为 std::intializer_list<int>,值为 { 1 }
auto d{ 1 }; // d 为 std::intializer_list<int>,值为 { 1 }
auto e = { 1, 2, 3 }; // e 为 std::intializer_list<int>
auto f = { 1, 2, 3.0 }; // 无法推导,编译错误

而在模板版类型推导中,却有

template<typename T>
void f1(T param);
template<typename T>
void f2(std::initializer_list<T> initList);

f1({ 1, 2, 3 }); // 无法推导,编译错误
f2({ 1, 2, 3 }); // T 为 int,initList 为 std::initializer_list<int>

在 C++14 中,允许 auto 表示推导的函数返回值,且lambda 可能会在参数声明里面使用 auto。但是此处的推导却直接使用的和模板一样的推导,而不是 auto 类型推导。

下面是一个例子:

auto a() {
    return { 1, 2, 3 }; // 无法推导,编译错误
}

auto b = [&c](const auto& d) { c = d; }
b({ 1, 2, 3 }); // 无法推导,编译错误

根据以上内容,我们可以总结:

  • auto 类型推导和模板类型推导几乎相同
  • auto 类型推导统一初始化为 std::initializer_list
  • auto 在函数返回值或者 lambda 参数里面执行模板类型推导

decltype 使用

decltype 用来判断变量或者表达式类型。它一般只是复述一遍你所给他的变量名或者表达式的类型。

下面是几个例子:

const int a = 0; // decltype(a) 为 const int
bool b(const int& c); // decltype(c) 为 const int&,decltype(b) 为 bool(const int&)
struct d{ int e, f; }; // decltype(d::e) 为 int
vector<int> g; // decltype(g) 为 vector<int>,decltype(g[0]) 为 int&

decltype 最主要的用处是声明一个函数模板,使得这个函数模板的返回值类型取决于参数的类型。

下面是一个例子:

template<typename Container, typename Index>
auto f(Container& c, Index i)
-> decltype(c[i]) {
    return c[i];
}

到了 C++14,我们这样写也是对的:

template<typename Container, typename Index>
auto f(Container& c, Index i) {
    return c[i];
} // 返回了 int

然而,这还是有问题。假设 c 中对象的类型为 int,则 c[i] 返回的类型为 int&,经过 auto 后,引用会被忽略,变为 int。这时,返回的就是右值而不是左值。如果想要返回左值,则必须这样写:

template<typename Container, typename Index>
decltype(auto) f(Container& c, Index i) {
    return c[i];
} // 返回了 int&

decltype(auto) 也可以使用在变量声明上。

下面是一个例子:

int a;
const int& b = a; // b 为 const int&

auto c = b; // c 为 int
decltype(auto) d = b; // d 为 const int&

上面讲的函数参数均为左值。如果希望有右值引用参数,则需要这样写:

template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i]) {
    return std::forward<Container>(c)[i];
}

在 C++14 中改为:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
    return std::forward<Container>(c)[i];
}

以上情况中,decltype 都是什么就输出什么。然而在某些情况下并非如此。

我们知道,如果给 decltype 一个类型为 T 的左值表达式,其会给出 T&

下面是一个例子:

decltype(auto) f1() {
    int x = 0;
    return x; // decltype(x) 为 int
}

decltype(auto) f2() {
    int x = 0;
    return (x); // decltype((x)) 为 int&
}

可以看到,仅仅一个左值与左值表达式的情况大相径庭。

根据以上内容,我们可以总结:

  • C++14 支持 decltype(auto),其推导 auto 类型时使用 decltype 的规则
  • 对于非变量名的、类型为 T 的左值表达式,decltype 返回 T&

查看类型推导结果

  • IDE 查看

    现在大部分 ide 都支持查看类型推导结果。例如,在 vscode 中,只需要在写代码时把鼠标移动到变量上,就能显示变量的类型。

    然而,如果类型较为复杂,ide 的显示结果很可能是错误的。

  • 编译器诊断

    比如我们定义了如下变量:

    const int a = 1;
    auto b = a;
    auto c = &a;
    

    我们可以利用编译器来查看变量类型:

    template<typename T>
    class TypeDisplay; 
      
    TypeDisplay<decltype(b)> bType;
    TypeDisplay<decltype(c)> cType;
    

    编译时,编译器会报错:

    error: aggregate 'TypeDisplay<int> bType' has incomplete type and cannot be defined
    error: aggregate 'TypeDisplay<const int*> cType' has incomplete type and cannot be defined
    

    这样就看到了 decltype 推导出的类型。

  • 运行时输出

    我们可以使用 typeid 来输出类型。

    下面是一个例子:

    const int a = 1;
    auto b = a;
    auto c = &a;
      
    std::cout << typeid(b).name() << '\n';
    std::cout << typeid(c).name() << '\n';
    

    运行后输出

    i
    PKi
    

    其中,i 表示 int,P 为 pointer,k 为 c(k)onst。

参考资料

  • Effective Modern C++, Scott Meyers

Comments

Share This Post