C++泛型编程指南08 auto decltype
文章目录
- @[TOC]
- 第3章:`auto`占位符(C++11~C++17)
- 3.1 `auto`关键字的重新定义
- 3.2 类型推导规则
- 3.3 何时使用`auto`
- 3.4 返回类型推导
- 3.5 在Lambda表达式中使用`auto`
- 3.6 非类型模板参数占位符
- 总结
- 第4章 `decltype`说明符(C++11~C++17)
- 4.1 回顾 `typeof` 和 `typeid`
- 4.2 使用 `decltype` 说明符
- 4.3 推导规则
- 4.4 cv限定符的推导
- 4.5 `decltype(auto)`
- 4.6 `decltype(auto)` 作为非类型模板形参占位符
- 总结
- 第5章 函数返回类型后置与推导
- 5.1 使用函数返回类型后置声明函数
- 5.2 推导函数模板返回类型
- 总结
文章目录
- @[TOC]
- 第3章:`auto`占位符(C++11~C++17)
- 3.1 `auto`关键字的重新定义
- 3.2 类型推导规则
- 3.3 何时使用`auto`
- 3.4 返回类型推导
- 3.5 在Lambda表达式中使用`auto`
- 3.6 非类型模板参数占位符
- 总结
- 第4章 `decltype`说明符(C++11~C++17)
- 4.1 回顾 `typeof` 和 `typeid`
- 4.2 使用 `decltype` 说明符
- 4.3 推导规则
- 4.4 cv限定符的推导
- 4.5 `decltype(auto)`
- 4.6 `decltype(auto)` 作为非类型模板形参占位符
- 总结
- 第5章 函数返回类型后置与推导
- 5.1 使用函数返回类型后置声明函数
- 5.2 推导函数模板返回类型
- 总结
第3章:auto
占位符(C++11~C++17)
3.1 auto
关键字的重新定义
虽然auto
关键字自C++98标准以来就已经存在,用于声明自动变量,但C++11为其赋予了新的意义:根据初始化表达式自动推断变量类型,或作为函数返回值类型的占位符。例如:
auto i = 5; // 推断为int
auto str = "hello auto"; // 推断为const char*
auto sum(int a1, int a2) -> int { return a1 + a2; } // 返回类型后置,使用auto作为占位符
重要的是,当编译器无法推导出类型时,使用auto
会导致编译失败。
这里有几点需要注意:
-
多变量声明:使用单个
auto
关键字声明多个变量时,编译器会依据最左边的初始化表达式来推导auto
的具体类型:int n = 5; auto *pn = &n, m = 10; // pn: int*, m: int
如果尝试将不同类型的值赋给同一
auto
声明的不同变量,会导致编译错误:int n = 5; auto *pn = &n, m = 10.0; // 编译失败,类型不匹配
-
条件表达式初始化:在使用条件表达式初始化
auto
变量时,编译器会选择表达能力更强的类型:auto i = true ? 5 : 8.0; // i的数据类型为double
-
成员变量声明限制:尽管C++11允许在声明成员变量时进行初始化,但不允许用
auto
声明非静态成员变量。然而,在C++11中,可以用const
限定符声明静态成员变量,并且在C++17中,即使没有const
限定符也可以这样做:struct sometype { static const auto i = 5; // C++11 static inline auto j = 5; // C++17 };
-
函数参数限制:在C++20之前,不能在函数形参列表中使用
auto
声明形参(但在C++14中,auto
可用于lambda表达式的形参):void echo(auto str); // C++20之前编译失败
3.2 类型推导规则
-
忽略cv限定符:对于按值初始化的
auto
变量,推导类型时会忽略常量和volatile限定符:const int i = 5; auto j = i; // auto推导为int,而非const int
-
引用属性被忽略:如果目标对象是引用,
auto
推导类型时会忽略引用属性:int i = 5; int &j = i; auto m = j; // auto推导为int,而非int&
-
万能引用:使用
auto&&
声明变量时,对于左值auto
会被推导为引用类型,而对于右值则不会:int i = 5; auto&& m = i; // auto推导为int&(引用折叠) auto&& j = 5; // auto推导为int
-
数组与函数指针:当目标对象是数组或函数时,
auto
会被推导为对应的指针类型:int i[5]; auto m = i; // auto推导为int* int sum(int a1, int a2); auto j = sum; // auto推导为函数指针类型
-
列表初始化:结合
auto
和列表初始化时,有特定的规则需遵守(C++17标准):- 使用直接列表初始化时,列表中必须是单元素,否则编译失败。
- 使用等号加列表初始化时,列表可以包含一个或多个相同类型的元素,此时
auto
类型会被推导为std::initializer_list<T>
。
auto x1 = { 1, 2 }; // x1类型为 std::initializer_list<int> auto x2{ 3 }; // x2类型为int
通过上述内容,我们可以更好地理解和运用auto
关键字,提高代码的灵活性和可读性。接下来的部分将探讨何时使用auto
以及更多高级特性。
3.3 何时使用auto
合理使用auto
可以提高代码的灵活性和可读性,尤其是在处理复杂类型或模板时。以下是几种推荐使用auto
的情况:
-
一眼能看出初始化类型的变量:当变量的初始化表达式已经清楚地表明了其类型时,使用
auto
可以使代码更简洁。std::map<std::string, int> str2int; // 填充str2int的代码 for (auto it = str2int.cbegin(); it != str2int.cend(); ++it) { // 处理每个元素 } // 或者使用范围for循环 for (auto &it : str2int) { // 处理每个元素 }
-
复杂的类型:对于那些类型声明冗长或难以书写的类型(如lambda表达式、
std::bind
等),直接使用auto
可以简化代码。auto l = [](int a1, int a2) { return a1 + a2; }; // lambda表达式 int sum(int a1, int a2) { return a1 + a2; } auto b = std::bind(sum, 5, std::placeholders::_1); // 使用std::bind
-
模板编程:在模板编程中,类型推导可以帮助减少重复代码,特别是在需要处理多种类型的场景下。
-
避免显式的类型转换:使用
auto
可以避免不必要的显式类型转换,使代码更加自然和易读。
3.4 返回类型推导
C++14引入了对函数返回类型自动推导的支持,允许将返回类型声明为auto
。这使得编写泛型代码变得更加简单:
auto sum(int a1, int a2) {
return a1 + a2; // 编译器会根据返回值推导出返回类型为int
}
需要注意的是,如果一个函数有多个返回路径,并且这些路径返回不同类型的值,则编译器无法进行类型推导,会导致编译错误。
3.5 在Lambda表达式中使用auto
C++14还扩展了lambda表达式的功能,允许在lambda表达式的形参列表中使用auto
,从而创建泛型lambda表达式:
auto l = [](auto a1, auto a2) { return a1 + a2; };
auto retval = l(5, 5.0); // a1被推导为int,a2被推导为double,retval被推导为double
此外,lambda表达式还可以通过auto
返回引用类型:
auto l = [](int &i) -> auto& { return i; };
auto x1 = 5;
auto &x2 = l(x1);
assert(&x1 == &x2); // 检查是否指向同一内存地址
这是让lambda表达式返回引用类型的唯一方法。
3.6 非类型模板参数占位符
C++17进一步扩展了auto
的功能,使其可以用作非类型模板参数的占位符。这意味着可以在模板声明中使用auto
作为参数类型:
#include <iostream>
template<auto N>
void f() {
std::cout << N << std::endl;
}
int main() {
f<5>(); // N为int类型
f<'c'>(); // N为char类型
// f<5.0>(); // 错误:模板参数不能为double
}
这个特性允许模板函数接受任何类型的常量作为参数,只要该类型支持作为模板参数。
总结
通过本章的学习,我们了解了auto
关键字在现代C++中的重要作用及其丰富的应用场景。从简化复杂类型声明到支持泛型编程,再到与lambda表达式的结合使用,auto
极大地增强了C++代码的灵活性和可维护性。合理利用这些特性,可以帮助开发者编写更加高效、简洁的代码。希望这些内容能帮助你在日常开发中更好地运用auto
,提升编码效率和代码质量。
第4章 decltype
说明符(C++11~C++17)
4.1 回顾 typeof
和 typeid
在C++11标准发布之前,GCC提供了一个名为typeof
的扩展运算符,用于获取操作数的具体类型。例如:
int a = 0;
typeof(a) b = 5; // b被推导为int类型
除了使用typeof
,C++标准还提供了typeid
运算符来获取与目标操作数类型相关的详细信息。这些信息存储在一个std::type_info
对象中,可以通过调用其成员函数name()
来获取类型的名称。例如:
int x1 = 0;
double x2 = 5.5;
std::cout << typeid(x1).name() << std::endl; // 输出 "int"
std::cout << typeid(x1 + x2).name() << std::endl; // 输出 "double"
std::cout << typeid(int).name() << std::endl; // 输出 "int"
关于typeid
的几点重要特性:
- 生命周期:
typeid
的返回值是一个左值,其生命周期持续到程序结束。 - 引用和指针:
std::type_info
删除了复制构造函数,因此只能通过引用或指针保存其结果:auto t1 = typeid(int); // 编译失败,没有复制构造函数 auto &t2 = typeid(int); // 编译成功,t2推导为const std::type_info& auto t3 = &typeid(int); // 编译成功,t3推导为const std::type_info*
- 忽略cv限定符:
typeid
忽略类型的cv限定符,即typeid(const T)
等同于typeid(T)
。
4.2 使用 decltype
说明符
C++11引入了decltype
说明符,用于获取对象或表达式的类型。其语法类似于typeof
:
int x1 = 0;
decltype(x1) x2 = 0; // x2被推导为int类型
double x3 = 0;
decltype(x1 + x3) x4 = x1 + x3; // x4被推导为double类型
// decltype({1, 2}) x5; // 编译失败,{1, 2}不是表达式
与auto
不同,decltype
可以在非静态成员变量中使用:
struct S1 {
int x1;
decltype(x1) x2; // x2被推导为int类型
double x3;
decltype(x2 + x3) x4; // x4被推导为double类型
};
在函数形参列表中使用decltype
:
int x1 = 0;
decltype(x1) sum(decltype(x1) a1, decltype(a1) a2) {
return a1 + a2;
}
auto x2 = sum(5, 10); // x2被推导为int类型
为了更好地讨论decltype
的优势,考虑以下例子:
auto sum(int a1, int a2) -> int {
return a1 + a2;
}
在C++11中,auto
作为占位符无法使编译器自动推导函数返回类型,必须使用返回类型后置的形式指定返回类型。如果想泛化这个函数以支持各种类型运算,需要使用模板:
template<class R, class T1, class T2>
R sum(T1 a1, T2 a2) {
return a1 + a2;
}
auto x3 = sum<double>(5, 10.5); // x3被推导为double类型
为了简化代码,可以进一步优化:
template<class T1, class T2>
auto sum(T1 a1, T2 a2) -> decltype(a1 + a2) {
return a1 + a2;
}
auto x4 = sum(5, 10.5); // x4被推导为double类型
在C++14中,可以直接使用auto
进行返回类型推导:
template<class T1, class T2>
auto sum(T1 a1, T2 a2) {
return a1 + a2;
}
auto x5 = sum(5, 10.5); // x5被推导为double类型
尽管C++14支持对auto
声明的返回类型进行推导,但在某些情况下仍然需要使用decltype
,例如返回引用类型时:
template<class T>
auto return_ref(T& t) -> decltype(t) {
return t;
}
int x1 = 0;
static_assert(std::is_reference_v<decltype(return_ref(x1))>); // 编译成功
4.3 推导规则
decltype(e)
(其中e的类型为T)的推导规则如下:
- 标识符表达式:如果e是一个未加括号的标识符表达式(结构化绑定除外)或者未加括号的类成员访问,则
decltype(e)
推断出的类型是e的类型T。如果不存在这样的类型,或者e是一组重载函数,则无法进行推导。 - 函数调用:如果e是一个函数调用或者仿函数调用,那么
decltype(e)
推断出的类型是其返回值的类型。 - 左值:如果e是一个类型为T的左值,则
decltype(e)
是T&。 - 将亡值:如果e是一个类型为T的将亡值,则
decltype(e)
是T&&。 - 其他情况:除去以上情况,则
decltype(e)
是T。
示例:
const int&& foo();
int i;
struct A {
double x;
};
const A* a = new A();
decltype(foo()); // const int&&
decltype(i); // int
decltype(a->x); // double
decltype((a->x)); // const double&
更多示例:
int i;
int *j;
int n[10];
const int&& foo();
decltype(static_cast<short>(i)); // short
decltype(j); // int*
decltype(n); // int[10]
decltype(foo); // 函数类型
struct A {
int operator() () { return 0; }
};
A a;
decltype(a()); // int
复杂表达式示例:
int i;
int *j;
int n[10];
decltype(i=0); // int&
decltype(0,i); // int&
decltype(i,0); // int
decltype(n[5]); // int&
decltype(*j); // int&
decltype(static_cast<int&&>(i)); // int&&
decltype(i++); // int
decltype(++i); // int&
decltype("hello world"); // const char(&)[12]
4.4 cv限定符的推导
通常情况下,decltype(e)
所推导的类型会同步e的cv限定符:
const int i = 0;
decltype(i); // const int
当e是未加括号的成员变量时,父对象表达式的cv限定符会被忽略:
struct A {
double x;
};
const A* a = new A();
decltype(a->x); // double
当e是加括号的数据成员时,父对象表达式的cv限定符会同步到推断结果:
struct A {
double x;
};
const A* a = new A();
decltype((a->x)); // const double&
4.5 decltype(auto)
C++14引入了decltype(auto)
组合,它告诉编译器使用decltype
的推导规则来推导auto
。需要注意的是,decltype(auto)
必须单独声明,不能结合指针、引用以及cv限定符:
int i;
int&& f();
auto x1a = i; // int
decltype(auto) x1d = i; // int
auto x2a = (i); // int
decltype(auto) x2d = (i); // int&
auto x3a = f(); // int
decltype(auto) x3d = f(); // int&&
auto x4a = { 1, 2 }; // std::initializer_list<int>
// decltype(auto) x4d = { 1, 2 }; // 编译失败, {1, 2}不是表达式
auto *x5a = &i; // int*
// decltype(auto)*x5d = &i; // 编译失败,decltype(auto)必须单独声明
简化返回类型后置的语法:
template<class T>
decltype(auto) return_ref(T& t) {
return t;
}
int x1 = 0;
static_assert(std::is_reference_v<decltype(return_ref(x1))>);
4.6 decltype(auto)
作为非类型模板形参占位符
在C++17中,decltype(auto)
也能作为非类型模板形参的占位符,其推导规则与前面介绍的一致:
#include <iostream>
template<decltype(auto) N>
void f() {
std::cout << N << std::endl;
}
static const int x = 11;
static int y = 7;
int main() {
f<x>(); // N为const int类型
f<(x)>(); // N为const int&类型
// f<y>(); // 编译错误,y不是一个常量
f<(y)>(); // N为int&类型
}
总结
通过本章的学习,我们了解了decltype
及其变体decltype(auto)
在现代C++中的重要作用及其应用场景。合理利用这些特性,可以帮助开发者编写更加高效、简洁和灵活的代码。无论是简化复杂的类型声明,还是处理泛型编程中的细节问题,decltype
都提供了强大的支持。希望这些内容能帮助你在日常开发中更好地运用decltype
,提升编码效率和代码质量。
第5章 函数返回类型后置与推导
5.1 使用函数返回类型后置声明函数
C++11引入了函数返回类型后置的语法,允许在函数声明中将返回类型放在参数列表之后。这种语法特别适用于返回类型复杂的场景,如返回函数指针类型。以下是基本示例:
auto foo() -> int {
return 42;
}
对于更复杂的返回类型,比如返回一个函数指针,返回类型后置可以显著提高代码的可读性:
int bar_impl(int x) {
return x;
}
typedef int(*bar)(int);
bar foo1() {
return bar_impl;
}
auto foo2() -> int(*)(int) {
return bar_impl;
}
int main() {
auto func = foo2();
func(58);
}
在这个例子中,foo2
使用返回类型后置的方式定义了一个返回类型为 int(*)(int)
的函数。
5.2 推导函数模板返回类型
在C++11标准中,函数返回类型后置的一个重要用途是推导函数模板的返回类型。通过结合 decltype
说明符,可以在编译时根据参数类型推导出返回类型。例如:
template<class T1, class T2>
auto sum1(T1 t1, T2 t2) -> decltype(t1 + t2) {
return t1 + t2;
}
int main() {
auto x1 = sum1(4, 2);
}
这里,decltype(t1 + t2)
用于推导 sum1
函数的实际返回类型。需要注意的是,decltype(t1 + t2)
不能写在函数声明之前,因为编译器在解析返回类型时还未解析到参数部分,因此无法识别 t1
和 t2
。
另一种方法是直接在函数声明前使用 decltype
,但这会导致需要调用类型的默认构造函数,这在某些情况下可能会导致编译失败。例如:
class IntWrap {
public:
IntWrap(int n) : n_(n) {}
IntWrap operator+ (const IntWrap& other) {
return IntWrap(n_ + other.n_);
}
private:
int n_;
};
template<class T1, class T2>
decltype(T1() + T2()) sum2(T1 t1, T2 t2) {
return t1 + t2;
}
int main() {
// 编译失败,IntWrap没有默认构造函数
sum2(IntWrap(1), IntWrap(2));
}
为了克服这个问题,可以使用以下两种方法:
- 使用指针转换和解引用:
template<class T1, class T2>
decltype(*static_cast<T1*>(nullptr) + *static_cast<T2*>(nullptr))
sum3(T1 t1, T2 t2) {
return t1 + t2;
}
这种方法通过将 nullptr
转换为 T1
和 T2
类型的指针,并解引用它们来进行求和操作。由于编译器不会真正执行这些操作,因此不会引发运行时错误。
- 使用
std::declval
:
std::declval
是标准库提供的一个工具函数,它将类型 T
转换为引用类型,而不需要实际构造对象。这使得在 decltype
中使用表达式成为可能,而无需依赖默认构造函数。
#include <utility> // 包含 std::declval
template<class T1, class T2>
decltype(std::declval<T1>() + std::declval<T2>())
sum4(T1 t1, T2 t2) {
return t1 + t2;
}
int main() {
sum3(IntWrap(1), IntWrap(2));
sum4(IntWrap(1), IntWrap(2));
}
这两种方法都能有效地解决在推导复杂返回类型时遇到的问题,同时保持代码的清晰和简洁。
总结
通过本章的学习,我们了解了如何使用C++11中的函数返回类型后置语法来简化复杂返回类型的声明。此外,我们还探讨了如何利用 decltype
和 std::declval
来推导函数模板的返回类型,从而避免因默认构造函数缺失而导致的编译错误。合理运用这些特性,可以使代码更加简洁、易读且功能强大。希望这些内容能帮助你在日常开发中更好地处理复杂返回类型的场景。