Effective Modern C++——型别推导

文章目录

  • 型别推导
    • 查看型别推导结果的方法
      • [IDE 编辑器](#IDE 编辑器)
      • 编译器诊断信息
      • 运行时输出
        • [typeid 和 std::type_info::name](#typeid 和 std::type_info::name)
        • [Boost 的 TypeIndex 库](#Boost 的 TypeIndex 库)
    • 模板型别推导
      • [ParamType 是个指针或引用,但不是个万能引用](#ParamType 是个指针或引用,但不是个万能引用)
      • [ParamType 是个万能引用](#ParamType 是个万能引用)
      • [ParamType 既非指针也非引用](#ParamType 既非指针也非引用)
      • 数组实参
      • 函数实参
    • [auto 型别推导](#auto 型别推导)
    • [decltype 型别推导](#decltype 型别推导)

型别推导

查看型别推导结果的方法

IDE 编辑器

IDE 中的代码编辑器通常会在你将鼠标指针悬停至某个程序实体,如变量、形参、函数等时,显示出该实体的型别。

cpp 复制代码
const int theAnswer=42;
//推导为int
auto x=theAnswer;
//推导为const int*
auto y=&theAnswer;


编译器诊断信息

想要让编译器显示其推导出的型别,一条有效的途径是使用该型别导致某些编译错误。

而报告错误的消息几乎肯定会提及导致该错误的型别。

cpp 复制代码
//只声明TD(Type Displayer)而不定义
template<typename T>
class TD;

int main(){
    const int theAnswer=42;
    //x推导为int
    auto x=theAnswer;
    //y推导为const int*
    auto y=&theAnswer;

    //诱发包括x和y的型别的错误消息
    TD<decltype(x)>xType;
    TD<decltype(y)>yType;
}


运行时输出

typeid 和 std::type_info::name

针对某个对象,如 x 和 y 调用 typeid,就得到了一个 std::type_info 对象,而后者拥有一个

cpp 复制代码
#include <iostream>
#include <vector>
#include <typeinfo> // 必须包含此头文件以使用 typeid

using namespace std;

class Widget {
public:
    Widget(int id) : id(id) {}
private:
    int id;
};

template<typename T>
void f(const T& param) {
    // 使用宏或特定的编译器技巧可以获得更易读的型别名称
    // 这里使用标准 typeid().name()
    cout << "T 类型推导为:     " << typeid(T).name() << endl;
    cout << "param 类型推导为: " << typeid(param).name() << endl;
    cout << "------------------------------------------" << endl;
}

// 工厂函数:返回一个包含 Widget 的 vector
std::vector<Widget> createVec() {
    std::vector<Widget> v;
    v.emplace_back(1);
    v.emplace_back(2);
    return v;
}

int main() {
    // 1. 使用工厂函数返回值初始化 vw
    // vw 的型别被推导为 const std::vector<Widget>
    const auto vw = createVec();

    // 2. 调用 f
    if (!vw.empty()) {
        // 我们传入的是 &vw[0],即一个指向 const Widget 的指针
        // 观察模板 f 如何推导 T 和 param 的型别
        f(&vw[0]); 
    }

    return 0;
}

输出结果为 PK6Widget,PK 表示"pointer to konst const"(指涉到常量的指针),6 表示后面类名的字符数。

**<font style="color:#DF2A3F;">std::type_info::name</font>**并不可靠 ,因为它处理型别的方式就仿佛是向函数模板按值传递传递形参一样,其引用性被忽略,实际上应该为 const Widget*const &性别,但被输出为 const Widget*

更不幸的是,IDE编辑器同它输出一样,并不可靠。

而更可靠的是使用 Boost 的 TypeIndex 库。

Boost 的 TypeIndex 库

函数模板 <font style="color:#DF2A3F;">boost::typeindex::type_id_with_cvr</font>接受一个型别实参(我们想要获取信息的型别),而不会移除 **<font style="color:#DF2A3F;">const</font>** **<font style="color:#DF2A3F;">volatile</font>**和引用饰词 ,并利用成员函数 <font style="color:#DF2A3F;">pretty_name</font>产生了一个包含人类可读的型别表示的 <font style="color:#DF2A3F;">std::string</font>

cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(const T&param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param"<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

class Widget {
public:
    Widget(int id) : id(id) {}
private:
    int id;
};

// 工厂函数:返回一个包含 Widget 的 vector
std::vector<Widget> createVec() {
    std::vector<Widget> v;
    v.emplace_back(1);
    v.emplace_back(2);
    return v;
}

int main() {
    // 1. 使用工厂函数返回值初始化 vw
    // vw 的型别被推导为 const std::vector<Widget>
    const auto vw = createVec();

    // 2. 调用 f
    if (!vw.empty()) {
        // 我们传入的是 &vw[0],即一个指向 const Widget 的指针
        // 观察模板 f 如何推导 T 和 param 的型别
        f(&vw[0]); 
    }

    return 0;
}

可以看到输出的型别和我们理想要的型别是一样的,说明用这种方式是要比 IDE 显示的型别信息更加可靠可信。

模板型别推导

模板的型别推导是现代 C++最广泛应用的特性之一 ------auto 的基础。如果你想要使用 C++11 中推导 auto 型别,那么了解模板的型别推导必然是其前提,此外,当模板型别推到规则应用于 auto 语境时,它们并不像应用于模板规则那么符合直觉。

函数模板及其调用大致形如:

cpp 复制代码
//函数模板
template<typename T>
void f(ParamType param);
//调用模板函数
f(expr);

在编译器编译期间,编译器会通过 expr的值推导两个型别:**T 的型别**和 ParamType 的型别。

这两个型别常常并不一样,原因在于 ++ParamType 常会包含如 const,voliate 和引用等一些修饰词++。

根据 ParamType 的形式大致可分为三种主流情况:

  1. ParamType 是个指针或引用,但不是个万能引用。
  2. ParamType是个万能引用。
  3. ParamType既非指针也非引用。

也另含一些边缘情况,比如传入的实参是数组 或者函数等。

ParamType 是个指针或引用,但不是个万能引用

📌**推导方式****:**

  1. 若 expr 具有引用型别,先将引用部分忽略。
  2. 尔后,对 expr 的型别和 ParamType 的型别执行模式匹配来决定 T的型别。
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T& param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //x的型别为int
    int x=27;
    f(x);
    //x的型别为const int
    const int cx=x;
    f(cx);
    //x的型别为const int&
    const int&rx=x;
    f(rx);
    return 0;
}
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(const T& param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //x的型别为int
    int x=27;
    f(x);
    //x的型别为const int
    const int cx=x;
    f(cx);
    //x的型别为const int&
    const int&rx=x;
    f(rx);
    return 0;
}
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T* param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //x的型别为int
    int x=27;
    f(&x);
    //px的型别为const int
    const int *px=&x;
    f(px);
    return 0;
}

ParamType 是个万能引用

📌**推导方式****:**

  1. 如果expr是个左值,T和ParamType都会被推导为左值引用。这个结果具有双重的奇特之处:首先,这是在模板型别推导中,T 被推导为引用型别的唯一情形。其次,尽管在声明时使用的是右值引用语法,它的型别推导结果却是左值引用。
  2. 如果exptr是个右值,则应用上一个情况。
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T&& param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //x的型别为int
    int x=27;
    f(x);
    //x的型别为const int
    const int cx=x;
    f(cx);
    //x的型别为const int&
    const int&rx=x;
    f(rx);
    f(27);
    return 0;
}

ParamType 既非指针也非引用

📌**推导方式(按值传递)****:**

  1. 一如之前,若 expr 具有引用型别,则忽略其引用部分。
  2. 忽略 expr 的引用性之后,若 expr 是个 const 对象,也忽略之。若其是个 volatile 对象,同忽略之。
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //x的型别为int
    int x=27;
    f(x);
    //x的型别为const int
    const int cx=x;
    f(cx);
    //x的型别为const int&
    const int&rx=x;
    f(rx);
    //ptr是个指涉到const对象的const指针
    //前一个const修饰ptr指向的对象,表示字符串不可修改
    //后一个const修饰ptr本身,表示ptr不可以修改指向对象
    const char*const ptr="hello world";
    //模板推动规则为:忽略指针的常量性
    //所以只会忽略后一个const,保留前一个const
    //推导的型别为const char*
    f(ptr);
    return 0;
}

数组实参

📌**推导方式****:**

  1. 当按值传递时,数组退化为指针。
  2. 当按引用传递时,推导为数组类型,该型别包含数组尺寸。
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //name的型别为const char[11]
    const char name[]="J.P.Briggs";
    f(name);
    //数组退化为指针
    const char*ptrToName=name;
    f(ptrToName);
    return 0;
}
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T& param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //name的型别为const char[11]
    const char name[]="J.P.Briggs";
    f(name);
    //数组退化为指针
    const char*ptrToName=name;
    f(ptrToName);
    return 0;
}

函数实参

📌**函数型别推导同数组型别推导一样**:

  1. 当按值传递时,函数 退化为指针。
  2. 当按引用传递时,推导为函数类型。
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

void someFunc(int,double){
    
}

int main() {
    f(someFunc);
    return 0;
}
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T& param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

void someFunc(int,double){

}

int main() {
    f(someFunc);
    return 0;
}

auto 型别推导

绝大多数情况,auto 的型别推导和**模板**型别推导一样,两者之间可以建立起一一映射,也确实存在双向的算法交换。

当某变量采用 auto 来声明时,auto 就扮演了模板中的T 的角色,而变量的型别饰词 则扮演的是 ParamType 的角色。

💥然而,只有一种情况例外:auto 会假定用大括号括起的初始化表达式代表一个 **<font style="color:#DF2A3F;">std::initializer_list</font>**,但模板型别推导却不会。(C++17 之后,为了减少困惑,规则改为:如果是单值的直接列表初始化(没有等号),则直接推导为该值的类型)

cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //型别为int
    auto x1=27;
    f(x1);
    //型别为int
    auto x2(27);
    f(x2);
    //型别为std::initializer_list<int>
    auto x3={27};
    f(x3);
    //C++11/C++14:型别为std::initializer_list<int>
    //C++17:型别为int
    auto x4{27};
    f(x4);
    return 0;
}
cpp 复制代码
#include<iostream>
#include<boost/type_index.hpp>
#include<vector>

template<typename T>
void f(T param){
    using boost::typeindex::type_id_with_cvr;

    //显示T的型别
    std::cout<<"T="<<type_id_with_cvr<T>().pretty_name()<<std::endl;
    //显示param的型别
    std::cout<<"param="<<type_id_with_cvr<decltype(param)>().pretty_name()<<std::endl;
}

int main() {
    //型别为int
    auto x={11,23,9};
    f(x);
    //auto 会假定用大括号括起的初始化表达式代表一个 std::initializer_list
    //但模板型别推导却不会
    //所以向模板直接给大括号表达式模板不认识,但auto会认它为一个容器
    //f({{11,23,9}});   //编译错误
    return 0;
}

⚠️在 C++14 中允许使用 auto 来说明函数返回值需要推导,而且 C++14 中的 lambda 表达式也会在形参声明中用到 auto。然而,这些 auto 用法是在使用模板型别推导而非 auto 型别推导。

所以,带有 auto 返回值的函数不能返回一个大括号括起的初始化表达式。

也不能将一个大括号括起的初始化表达式作为 lambda 的形参型别。

decltype 型别推导

对于给定的名字或表达式,decltype 能给出该名字或表达式的型别。

大多数情况它会给定确切的型别, 它不会像 <font style="color:#000000;">auto</font> 那样剥离 <font style="color:#000000;">const</font><font style="color:#000000;">volatile</font> 或引用(reference)修饰符 :

cpp 复制代码
#include <vector>
#include <iostream>

// 定义一个简单的结构体和类
struct Point {
    int x, y; // decltype(Point::x) 是 int
};

class Widget {};

// 声明函数 f
bool f(const Widget& w); // decltype(f) 是 bool(const Widget&)

int main() {
    // --- 基础类型 ---
    const int i = 0;          // decltype(i) 是 const int

    // --- 对象与函数调用 ---
    Widget w;                 // decltype(w) 是 Widget
    
    if (f(w)) {               // decltype(f(w)) 是 bool
        // ...
    }

    // --- 容器与成员访问 ---
    // 模拟 std::vector 的简化逻辑
    std::vector<int> v;       // decltype(v) 是 std::vector<int>

    if (!v.empty()) {
        // 重点:vector 的 operator[] 通常返回的是引用
        // 所以 decltype(v[0]) 是 int&
        if (v[0] == 0) {
            // ...
        }
    }

    return 0;
}
cpp 复制代码
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) {
    // 逻辑处理...
    return c[i]; // 推导为引用,如 int&
}
cpp 复制代码
template<typename Container, typename Index>
decltype(auto) authAndAccessV2(Container& c, Index i) {
    return c[i]; // 完美保留了 c[i] 的引用特性
}

// 示例应用
std::vector<int> v = {1, 2, 3};
authAndAccessV2(v, 0) = 10; // 正确:返回的是 int&,可以赋值

⚠️**特殊情况 (双括号陷阱) :**

  • 对于一个名字(id-expression),<font style="color:#000000;">decltype</font> 返回该名字的类型。
  • 对于一个左值表达式,如果它不是简单的名字,<font style="color:#000000;">decltype</font> 会推导出该类型的引用。例如 <font style="color:#000000;">decltype((x))</font> 永远是引用,而 <font style="color:#000000;">decltype(x)</font> 取决于 <font style="color:#000000;">x</font> 本身。
cpp 复制代码
int x = 0;

decltype(x)   y = x;   // y 的类型是 int
decltype((x)) z = x;   // z 的类型是 int& !!

// 警告:在函数中使用时
decltype(auto) f1() {
    int x = 0;
    return x;          // 返回 int
}

decltype(auto) f2() {
    int x = 0;
    return (x);        // 灾难!返回了局部变量的引用 (int&),会导致未定义行为
}
相关推荐
Hello eveybody2 小时前
介绍一下背包DP(C++)
开发语言·c++·动态规划·dp·背包dp
charlie1145141912 小时前
AwesomeQt:最小的Qt6系列迷你版本教程发布!
linux·c++·qt·c
Run_Teenage2 小时前
Linux:线程互斥,线程锁
运维·开发语言·jvm
小小de风呀2 小时前
de风——【从零开始学C++】(四):类和对象(下)
开发语言·c++·算法
覆东流2 小时前
第10天:python元组
开发语言·后端·python
CSCN新手听安2 小时前
【Qt】系统相关(一)内容简介,事件概念,事件的处理
开发语言·c++·qt
不想写代码的星星2 小时前
重识 std::tuple:一个被低估的编译期异构容器
开发语言·c++
techdashen3 小时前
用 Rust 写生产级服务要踩多少坑——Cloudflare 把答案做成了一个开源库
开发语言·rust·开源
码界奇点3 小时前
基于Python的微信公众号爬虫系统设计与实现
开发语言·爬虫·python·毕业设计·web·源代码管理