代码仓库shanchuann/CPP-Learninng

在 C++11 标准之前,开发者不得不面对冗长且易出错的类型声明 —— 无论是std::vector<std::map<std::string, int>>::iterator这样的复杂容器迭代器,还是依赖模板参数的动态类型,都需要手动精确指定。这种方式不仅降低了开发效率,更增加了代码维护的成本。为解决这一痛点,C++11 引入了autodecltype两个关键字,将类型推导的工作交给编译器,实现了代码的简化与灵活性的提升。

auto 是 C++11 起引入的占位符类型说明符,核心作用是让编译器从初始化器或表达式中推导类型,简化代码编写;C++14 起新增decltype(auto)变体,进一步扩展了类型推导的精确性。其语法、用法及约束均围绕 “从上下文推导类型” 展开,适用于变量、函数、参数等多种场景。

auto

auto 的语法分为两种形式,支持 C++20 起的 “类型约束”(基于概念)来限制推导范围:

基础形式:可选类型约束 + auto

1
const auto x = 5;
1
std::integral auto y = 10; //C++20,约束 y 为整数类型

decltype(auto)形式(C++14 起):可选类型约束 + decltype(auto)

1
decltype(auto) ref = (x);

推导结果与decltype((x))一致),且decltype(auto)必须是声明类型的唯一组成部分,不能搭配const /&等修饰符。

其中,“类型约束” 是 C++20 新增特性:若约束为Concept<A1,...,An>,则推导类型T需满足Concept<T, A1,...,An>;若约束无效或返回false,推导失败。

auto 的核心用法

参数声明

auto 作为参数类型时,主要用于泛型或简化参数声明,需注意版本限制:

  • auto作形式参数C++20 起支持的 “简写函数模板”,表面上是用auto作为函数形参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void Autofunc(auto x) {
    cout << x << endl;
    }
    int main() {
    Autofunc(10);
    Autofunc(10.5);
    Autofunc("hello");
    return 0;
    }

    auto x中的auto并非 “动态类型”,而是 编译期根据传入的实参类型,静态推导模板参数T的类型,推导逻辑与模板参数推导完全一致。

    1. 调用Autofunc(10):实参是int类型 → 推导T = int → 实际调用Autofunc<int>(10)
    2. 调用Autofunc(10.5):实参是double类型 → 推导T = double → 实际调用Autofunc<double>(10.5)
    3. 调用Autofunc("hello"):实参是const char*类型(字符串字面量的类型) → 推导T = const char* → 实际调用Autofunc<const char*>("hello")

    注意:不允许在结构体或类型命中使用auto

  • 泛型 lambda(C++14 起):lambda 参数用 auto 时,该 lambda 成为泛型 lambda,支持多种类型参数。

    1
    auto add = [](auto a, auto b) { return a + b; }; //可接收 int、double 等类型
  • 非类型模板参数(C++17 起):非类型模板参数用 auto 时,类型从模板实参推导。

    1
    template<auto N> auto get_val() { return N; }

调用 get_val<5>() 时,N 类型为 int

  • 简写函数模板(C++20 起):函数参数用 auto 时,该函数自动成为模板函数,无需显式声明模板参数。

    1
    auto multiply(auto a, auto b) { return a * b; }

    等价于

    1
    template<typename T, typename U> auto multiply(T a, U b) {...}

函数声明

auto 用于函数返回类型时,推导规则随 C++ 版本升级简化:

  • C++11:仅支持 “尾随返回类型”(需搭配->指定推导依据),不能直接用 auto 推导返回类型。

    1
    auto add(int a, double b) -> decltype(a + b) { return a + b; } //(返回类型由a + b推导)
  • C++14 起:支持直接用 auto 推导返回类型,无需尾随返回类型;未定义函数(仅声明)的返回类型,推导延迟到函数定义时。

    1
    auto func() { return 3.14; } //推导为 double
    1
    auto func(); //声明有效,定义时推导类型

变量声明

这是 auto 最常用的场景,核心规则是 “从初始化器推导类型”,需满足以下要求:

  • 变量必须有初始化器,不能单独声明auto x;(C++ 不允许,C 语言允许);

  • 可搭配const/&等修饰符,修饰符参与类型推导(如const auto& ref = x;推导为 const 引用);

  • 同一声明中多个变量的推导类型必须一致,否则编译错误。

    1
    auto a = 5, b = 10.5; //错误,a 为 int,b 为 double
    1
    const auto* p = &a, q = 6; //正确,p 为 const int*,q 为 const int
    1
    2
    3
    4
    5
    int main() {
    auto b = 5;
    //auto a = 5, b = 10.5; //错误,auto只能推导出一种类型
    auto* p = &b, a = 10; //&x被推导为int*, a被推导为int,当p变为*p时auto被推导为int
    }

auto&与const

1
2
3
4
5
6
int main() {
const int ca = 10;
auto a = ca; //a被推导为int类型
auto b = &ca; //b被推导为const int*类型
auto &c = ca; //c被推导为const int&类型
}
对顶层const

auto推导默认剥离“顶层const”

  • 顶层const定义:指“变量本身是const”,比如const int ca = 10中,ca的const修饰的是变量自身,即ca的值不能被修改,这就是顶层const。
  • auto推导逻辑:当auto直接推导“非指针、非引用”的变量时,会自动忽略初始化表达式的顶层const,只保留变量的基础类型。
  • 代码推导过程
    初始化器ca的类型是const int(顶层const),auto推导时剥离顶层const,因此a的最终类型是int(而非const int)。
    → 后续可修改a的值(如a = 20;是合法的),但不能修改caca = 20;会编译报错)。
对底层const

auto推导会保留“底层const”(指针/引用指向的对象是const)

  • 底层const定义:指“指针或引用指向的对象是const”,比如&ca的结果是“指向const int的指针”,这里的const修饰的是“指针指向的内容”,而非指针本身,这就是底层const。
  • auto推导逻辑:当auto推导的表达式是“指针”时,会保留表达式中的底层const,确保指针指向的对象属性不被破坏。
  • 代码推导过程
    &ca的类型是const int*(指针指向的ca是const,即底层const),auto推导时保留该底层const,因此b的最终类型是const int*
    → 后续可修改指针b的指向(如const int cb = 20; b = &cb;合法),但不能通过b修改指向的内容(*b = 30;会编译报错)。
auto&与const的强制保留规则

auto结合引用(auto&)时,会完整保留初始化表达式的const属性

  • 引用的特性前提:引用必须与被引用对象的“cv限定符(const/volatile)”完全匹配,不能用非const引用绑定到const对象(否则会破坏const对象的不可修改性)。
  • auto&推导逻辑:当auto后加&(声明引用)时,推导不再忽略const,而是完整继承初始化表达式的所有cv限定符,确保引用绑定合法。
  • 代码推导过程
    被引用的caconst intauto&需要继承其const属性,因此c的最终类型是const int&(const int的引用)。
    → 后续既不能通过c修改ca的值(c = 30;报错),也不能将c绑定到其他非const对象(如int x = 5; c = x;报错)。

结构化绑定声明(C++17 起)

auto 用于结构化绑定时,可解构数组、tuple、pair 或具有公共数据成员的类,将成员绑定到变量。例:auto [v, w] = std::make_pair(1, 3.14);(v 推导为 int,w 推导为 double)、int arr[2] = {10, 20}; auto [x, y] = arr;(x=10,y=20)

new 表达式

auto 可用于new表达式中,推导动态分配对象的类型(从初始化值推导)。例:auto p = new auto(42);(推导为 int*,动态分配 int 类型并初始化为 42)、auto arr = new auto[3]{1,2,3};(推导为 int*,动态分配 int 数组)

1
2
auto p = new int(20); //p被推导为int*
auto s = new auto(30); //s被推导为int*

函数式转换(C++23 起)

auto 可作为函数式转换的类型说明符,用于简化类型转换的写法。例:auto(5.5)(等价于static_cast<double>(5.5),推导为 double 类型)

版本兼容性:C++11 前 auto 是 “存储期说明符”(与register功能类似),C++11 起废弃该用法,仅作为占位符类型说明符;

初始化列表约束auto x = {1,2};推导为std::initializer_list<int>,但auto x{1,2};在 C++11 中为std::initializer_list<int>,C++14 起按 DR n3922 修复为 “编译错误”(需单元素);decltype(auto)不能用初始化列表推导(如decltype(auto) x = {1};错误);

多实体声明限制:同一声明中不能同时声明函数和变量(如auto f() -> int, x = 5;错误,CWG 1265 修复);

函数指针推导:C++11 曾禁止函数指针的返回类型用 auto 推导,CWG 2476 修复后,允许从初始化器推导(如auto (*fp)() = func;,fp 类型由 func 推导)。

decltype

decltype是 C++11 引入的类型查询运算符,核心作用是 “查询实体的声明类型或表达式的类型与值类别”,推导过程中不执行表达式。C++14 起扩展出decltype(auto)变体,C++20 起支持 “类型约束” 限制推导范围,其语法与推导逻辑围绕 “精准捕获类型” 设计,是泛型编程的核心工具。

语法

decltype的语法分为基础形式与扩展形式(含类型约束、decltype(auto)),不同形式对应不同版本支持与使用限制:

基础形式:decltype(表达式)

语法格式为decltype(e),其中e可是实体名(变量、类成员)或任意表达式(算术运算、函数调用等),推导结果为e的类型(含 cv 限定符、引用属性)。

1
2
3
4
5
6
7
8
9
10
11
int x = 10;
decltype(x) y = 20; //y被推导为int类型
decltype(x + y) z = 30; //z被推导为int类型

int& rx = x;
decltype(x) a; //a被推导为int类型
decltype(rx) b = x; //b被推导为int&类型

const int& crx = x;
decltype(crx) rb = x; //rb被推导为const int&类型
auto& b = crx; //b被推导为const int&类型

带类型约束的形式(C++20 起)

可在decltype前添加 “概念(Concept)” 作为类型约束,要求推导的类型满足概念条件,否则编译失败。语法格式为Concept decltype(e)或Concept<参数> decltype(e)。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <concepts>  // 需包含概念库

// 约束:推导的类型必须是整数类型
std::integral decltype(5 + 3) d; // 合法:5+3类型为int,满足std::integral
// std::floating_point decltype(5 + 3) e; // 错误:int不满足std::floating_point

// 约束:推导的类型必须是T的指针
template <typename T>
using Ptr = T*;
template <typename T>
concept PtrTo = std::is_pointer_v<T>;

PtrTo decltype(&x) f = &x; // 合法:&x类型为int*,满足PtrTo

decltype (auto) 变体(C++14 起)

decltype(auto)是基于decltype推导规则的 “简化写法”,语法上需作为声明类型的唯一组成部分,不能搭配**const/&/***等修饰符,推导逻辑完全遵循decltype的规则(而非auto)。

1
2
3
4
5
6
7
8
int x = 42;
decltype(auto) g = x; // 等价于decltype(x) g = x → int
decltype(auto) h = (x); // 等价于decltype((x)) h = (x) → int&
decltype(auto) i = &x; // 等价于decltype(&x) i = &x → int*

// 错误写法:decltype(auto)不能加修饰符
// const decltype(auto) j = x;
// decltype(auto)& k = x;

推导规则

decltype的推导结果由e的 “语法形式” 与 “值类别” 共同决定:

e 是 “无括号的 id 表达式”(变量名、类成员名)

“id 表达式” 指直接引用实体的名称(无额外括号、运算),此时decltype(e)直接返回实体的声明类型,完整保留 cv 限定符(const/volatile)与引用属性,不做任何修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 普通变量
const int ci = 10;
volatile double vd = 3.14;
decltype(ci) a; // const int(保留const)
decltype(vd) b; // volatile double(保留volatile)

// 2. 引用变量
int x = 42;
int& ref_x = x;
const int& cref_x = x;
decltype(ref_x) c = x; // int&(保留引用与非const)
decltype(cref_x) d = x; // const int&(保留引用与const)

// 3. 类成员
struct Data {
int val;
const std::string str;
};
Data data{5, "C++"};
decltype(data.val) e; // int(成员val的声明类型)
decltype(data.str) f; // const std::string(成员str的声明类型)

e 是 “表达式”(含括号、运算、函数调用等)

若e不是无括号的 id 表达式(如(x)、x+1、ptr),则decltype(e)的结果由*表达式的 “值类别” 决定:

  • 若e是左值(可寻址的对象,如变量、解引用指针、++x):推导为T&(左值引用);

  • 若e是亡值(即将销毁的临时对象,如std::move(x)、返回右值引用的函数调用):推导为T&&(右值引用);

  • 若e是纯右值(临时对象、字面量、x++):推导为T(非引用类型)。

表达式类型 值类别 decltype 推导结果 代码示例
带括号的变量 左值 T& decltype((x)) → int&
前缀自增(++x) 左值 T& decltype(++x) → int&
解引用(*ptr) 左值 T& decltype(*ptr) → int&
std::move(x) 亡值 T&& decltype(std::move(x)) → int&&
后缀自增(x++) 纯右值 T decltype(x++) → int
算术运算(x+3) 纯右值 T decltype(x+3) → int
字面量(5.5) 纯右值 T decltype(5.5) → double
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int x = 42;
int* ptr = &x;

// 左值表达式 → 推导为左值引用
decltype((x)) a = x; // int&
decltype(++x) b = x; // int&(++x返回x本身,左值)
decltype(*ptr) c = x; // int&(*ptr返回ptr指向的对象,左值)

// 亡值表达式 → 推导为右值引用
decltype(std::move(x)) d; // int&&(std::move返回亡值)
decltype(std::string("abc")) e; // std::string&&(临时字符串是亡值)

// 纯右值表达式 → 推导为非引用类型
decltype(x++) f; // int(x++返回临时值,纯右值)
decltype(3 + 4) g; // int(算术运算结果是纯右值)
decltype(ptr + 1) h; // int*(指针加法返回新指针,纯右值)

e 是 “函数调用 / 重载运算符”

此时decltype(e)推导为函数的返回类型,推导过程中不执行函数(仅分析返回类型),且保留返回类型的 cv 限定符与引用属性。若函数重载,需确保表达式能唯一确定重载版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 普通函数
int foo(); // 返回int(纯右值)
const double& bar(); // 返回const double&(左值)
void baz(int); // 返回void

decltype(foo()) a; // int(不执行foo(),仅推导返回类型)
decltype(bar()) b = bar(); // const double&(保留引用与const)
decltype(baz(5)) c; // void(推导为void类型)

// 2. 重载函数(需唯一匹配)
int add(int, int);
double add(double, double);
decltype(add(1, 2)) d; // int(匹配int版本)
decltype(add(1.5, 2.5)) e; // double(匹配double版本)

// 3. 重载运算符(等价于函数调用)
std::string s = "C++";
decltype(s + "11") f; // std::string(s+"11"调用operator+,返回临时对象)
decltype(s[0]) g = s[0]; // char&(s[0]调用operator[],返回左值引用)

函数类型由返回类型和参数表构成

用法

decltype的价值在于 “精准捕获类型”,尤其适用于auto无法满足的场景(如保留引用、不退化数组类型),核心用法集中在泛型编程、类型定义、函数返回类型等领域:

泛型函数的返回类型后置(与 auto 配合)

C++11 不支持auto直接推导泛型函数的返回类型(需 C++14),但可通过 “返回类型后置语法(trailing return type)” 结合decltype,让返回类型依赖函数参数的表达式类型,解决 “返回类型由参数运算决定” 的问题。

1
2
3
4
5
6
7
8
9
10
// 泛型函数:返回两个参数相加的结果,类型由t+u决定
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { // 后置返回类型,由t+u推导
return t + u;
}

// 调用示例
auto int_sum = add(2, 3); // 推导为int(2+3是int)
auto double_sum = add(2.5, 3); // 推导为double(2.5+3是double)
auto str_sum = add(std::string("a"), "b"); // 推导为std::string

模板中获取 “关联类型”

在模板编程中,常需根据模板参数的 “表达式类型” 推导关联类型(如容器元素类型、函数参数类型),decltype可精准捕获这些类型,避免手动指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 模板函数:打印容器的所有元素,用decltype获取迭代器解引用类型
template <typename Container>
void print_container(const Container& container) {
// 推导容器迭代器类型(等价于typename Container::iterator)
using It = decltype(container.begin());
// 推导容器元素类型(迭代器解引用的类型)
using ElemType = decltype(*container.begin());

for (It it = container.begin(); it != container.end(); ++it) {
ElemType elem = *it; // 精准匹配元素类型
std::cout << elem << " ";
}
std::cout << std::endl;
}

// 调用示例:支持vector、list等任意容器
std::vector<int> vec{1,2,3};
std::list<std::string> lst{"C++", "11", "decltype"};
print_container(vec); // 输出"1 2 3 "
print_container(lst); // 输出"C++ 11 decltype "

定义复杂类型的别名(简化代码)

对于数组、函数指针、嵌套容器等复杂类型,decltype可替代冗长的手动类型声明,通过表达式直接推导类型别名,提升代码可读性与可维护性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 推导数组类型(不退化,与auto不同)
int arr[5] = {1,2,3,4,5};
using ArrType = decltype(arr); // 推导为int[5](数组类型,不退化)
ArrType arr2 = {6,7,8,9,10}; // 合法:arr2是int[5]类型
// auto arr3 = arr; → 推导为int*(数组退化,对比差异)

// 2. 推导函数指针类型
void func(int, double);
using FuncPtrType = decltype(&func); // 推导为void(*)(int, double)
FuncPtrType fp = &func; // 合法:fp是函数指针
fp(10, 3.14); // 调用func

// 3. 推导嵌套容器类型
std::map<std::string, std::vector<int>> data{
{"a", {1,2}}, {"b", {3,4}}
};
// 推导map的value_type(std::pair<const std::string, std::vector<int>>)
using MapValType = decltype(*data.begin());
// 推导vector的元素类型(int)
using VecElemType = decltype(data["a"][0]);

处理 “表达式类型依赖” 场景

当变量 / 函数的类型依赖于某个动态表达式(如模板参数的运算、条件判断的结果),decltype可实时捕获表达式类型,无需提前预判。

1
2
3
4
5
6
7
8
9
// 模板:根据条件返回不同类型的结果,用decltype推导返回类型
template <typename T, typename U>
auto get_result(T t, U u, bool is_add) -> decltype(is_add ? t + u : t * u) {
return is_add ? t + u : t * u;
}

// 调用示例:根据is_add的值,返回不同类型
auto res1 = get_result(2, 3, true); // 2+3=5 → int
auto res2 = get_result(2.5, 3, false); // 2.5*3=7.5 → double

decltype (auto) 的特殊用法

C++14 的decltype(auto)适用于 “既想简化写法,又需保留表达式类型” 的场景,常见于变量声明与函数返回(尤其泛型函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 变量声明:保留表达式的引用/const属性
const int ci = 10;
decltype(auto) a = ci; // const int(保留const)
decltype(auto) b = (ci); // const int&(保留左值引用)

// 2. 泛型函数返回:精准传递返回值类型(避免auto的引用剥离)
template <typename T>
decltype(auto) get_ref(T& t) { // 返回T&(若t是const T&,则返回const T&)
return t;
}

const int& cref = ci;
auto ref1 = get_ref(ci); // auto推导为int(剥离const与引用)
decltype(auto) ref2 = get_ref(ci); // 推导为const int&(保留属性)