C++11新特性(一)——自动类型推导

1. auto

在C++11之前,autostatic 是对应的,表示变量自动存储,但是非 static 的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋,但是,在C++11中他们被赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型

cpp 复制代码
void func() {
	auto int a = 0;		// C++11以前:显式声明为自动存储(默认就是auto,一般不写)
	static int b = 0;	// 静态存储,生命周期持续到程序结束
	a++;
	b++;
	cout << "auto a=" << a << " static int b=" << b << endl;
}

int main() {
	func();			// auto a=1 static int b=1
	func();			// auto a=1 static int b=2
	func();			// auto a=1 static int b=3
	return 0;
}

1.1 推导规则

从上面的案例能发现,在 C++11 以前,auto 并不代表一种实际的数据类型,只是一个类型声明的"占位符",auto 并非在任意场景下都能够推导出变量的实际类型。使用 auto 声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将 auto 占位符替换为真正的类型。 使用语法如下:

cpp 复制代码
auto 变量名 = 变量值;

下面举个例子

cpp 复制代码
auto x = 10;		// x是int类型
auto y = 3.1415;	// y是double类型
auto z = 'a';		// z是char类型
auto a;				// 报错,没有进行初始化
auto double x;		// 报错,不能与任何其他类型组合(不能修改数据类型)

auto 还可以和指针、引用结合起来使用也可以带上 const、volatile 限定符,在不同的场景下有对应的推导规则,规则内容如下:

  • 当待推导的变量不是指针或引用类型时,推导结果中不会保留const、volatile关键字
  • 当待推导的变量是指针或引用类型时,推导结果中会保留const、volatile关键字

光是看上面的文字就觉得有些抽象,下面通过几个例子感受一下,第一个例子先介绍变量带指针和引用并使用auto进行类型推导:

cpp 复制代码
int temp = 110;
auto* a = &temp;	// &temp: int* --> auto* = int* --> auto: int
auto b = &temp;		// &temp: int* --> auto: int* 
auto& c = temp;		// auto: int
auto d = temp;		// auto: int

第二个例子介绍的是带 const 限定的变量,并使用 auto 进行类型推导

cpp 复制代码
int tmp = 250;
const auto a1 = tmp;	// auto: int
auto a2 = a1;			// a1: const int --> auto: int
const auto& a3 = tmp;	// a3: const int&
auto& a4 = a3;			// a4: const int&
auto* pt4 = &a1;		// pt4: const int*	
  • 变量a1的数据类型为const int,因此auto关键字被推导为int类型
  • 变量a2的数据类型为int,但是a2没有声明为指针或引用,因此const属性被去掉, auto被推导为int
  • 变量a3的数据类型为const int&,a3被声明为引用,因此const属性被保留,auto关键字被推导为int类型
  • 变量a4的数据类型为const int&,a4被声明为引用,因此const属性被保留,auto关键字被推导为const int类型
  • 变量pt4的数据类型为const int*,pt4被声明为指针,因此推导结果保留const,auto关键字被推导const int类型

1.2 auto的限制

auto 关键字并不是万能的,在某些场景下不能完成类型推导

  1. 不能作为函数的参数使用。因为只有在函数调用的时候才会给函数参数传递实参,但是auto要求必须要初始化赋值,因此,二者矛盾

    cpp 复制代码
    int func(auto a, auto b) {      //报错:此处不能使用auto
    	cout << "a = " << a << ",b = " << b << endl;
    }
  2. 不能用于类的非静态成员变量的初始化。因为类的非静态成员变量不属于类,它属于对象的,只有当这个类被创建出来后才能给该成员变量赋值。

    cpp 复制代码
    class Test {
    	auto v1 = 0;				// 报错
    	static auto v2 = 0;			// 报错 类的静态非常量成员不允许在类内部直接初始化(基础语法)
    	static const auto v3 = 10;	// 正确,静态常量在编译期可确定值,因此可以在类内初始化
    };
  3. 不能使用 auto 关键字定义数组

    cpp 复制代码
    int func() {
    	int array[] = { 1,2,3,4,5 };		
    	auto t1 = array;				// 正确 auto: int*
    	auto t2[] = array;				// 报错
    	auto t3[] = { 1,2,3,4,5 };		// 报错
    }
  4. 无法使用auto推导出模板参数

    cpp 复制代码
    template <typename T>
    struct Test{}
    
    int func()
    {
    	Test<double> t;
    	Test<auto> t1 = t;      // 报错 无法推导出模板类型
    	return 0;
    }

1.3 auto的应用

  1. 用于STL容器的遍历

    cpp 复制代码
    int main() {
    	map<int, string>mp;
    	mp.insert(make_pair(1, "ace"));
    	mp.insert(make_pair(2, "sabo"));
    	mp.insert(make_pair(3, "luffy"));
    	for (auto it = mp.begin(); it != mp.end(); it++) {
    		cout << it->first << ": " << it->second << endl;
    	}
    }
  2. 用于泛型编程。在使用模板的时候,大多数情况下我们不知道该定义什么类型的变量,例如:

    cpp 复制代码
    class T1 {
    public:
    	static int get() {
    		return 10;
    	}
    };
    
    class T2 {
    public:
    	static string get() {
    		return "hello world";
    	}
    };
    
    template<typename A>
    void func() {
    	auto ret = A::get();
    	cout << "ret: " << ret << endl;
    }
    
    int main() {
    	func<T1>();
    	func<T2>();
    	return 0;
    }

    在这个例子中定义了模板函数 func,在函数内部调用类A的静态方法 get(),但是我们不知道调用的是哪一个返回值的 get() 方法,因此返回值的类型不能确定,如果不使用auto,就需要再定义一个模板参数 typenameB,并且要在外部调用时手动指定get的返回值类型,这就非常麻烦。具体代码如下:

    cpp 复制代码
    template <typename A, typename B>        // 添加了模板参数B
    void func(void)
    {
    	B val = A::get();
    	cout << "val: " << val << endl;
    }
    
    int main()
    {
    	func<T1, int>();                 
    	func<T2, string>();              
    	return 0;
    }

2. decltype

在编译时期进行自动类型推导。引入 decltype 是因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来很不方便,甚至压根无法使用。 decltype 语法格式如下:

cpp 复制代码
decltype(表达式)

decltype 的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:

cpp 复制代码
int a = 10;
decltype(a) b = 100;			// b->int
decltype(a + 3.14) c = 3.14;	// c->double

2.1 推导规则

  1. 表达式为普通变量或普通表达式或类成员表达式,在这种情况下,使用 decltype 推导出的类型和表达式的类型是一致的

    cpp 复制代码
    class Test {
    public:
    	string text;
    	static const int value = 1;
    };
    
    int main() {
    	int x = 20;
    	const int& y = x;				// y -> const int&
    	decltype(x) a = x;				// a -> int
    	decltype(y) b = x;				// b -> const int&
    	decltype(Test::value) c = 0;	// c -> const int
    
    	Test t;
    	decltype(t.text) d = "hello";	// d -> string
    	return 0;
    }
  2. 表达式是函数调用,则 decltype 推导的类型应和函数返回值类型一致

    cpp 复制代码
    class Test{...};
    //函数声明
    int func_int();                 // 返回值为 int
    int& func_int_r();              // 返回值为 int&
    int&& func_int_rr();            // 返回值为 int&&
    
    const int func_cint();          // 返回值为 const int
    const int& func_cint_r();       // 返回值为 const int&
    const int&& func_cint_rr();     // 返回值为 const int&&
    
    const Test func_ctest();        // 返回值为 const Test
    
    //decltype类型推导
    int n = 100;
    decltype(func_int()) a = 0;			// a: int
    decltype(func_int_r()) b = n;		// b: int&
    decltype(func_int_rr()) c = 0;		// c: int&&
    decltype(func_cint())  d = 0;		// d: int   纯右值,此处忽略const
    decltype(func_cint_r())  e = n;		// e: const int&
    decltype(func_cint_rr()) f = 0;		// f: const int&&
    decltype(func_ctest()) g = Test();	// g: const Test  纯右值,但是是类类型,可以保留const

    函数 func_cint() 返回的是一个纯右值,对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符,因此推导出的变量 d 的类型为 int 而不是 const int

  3. 表达式是一个左值,或者被括号()包围,使用 decltype 推导出的是表达式类型的引用(如果有 const、volatile 限定符不能忽略)

    cpp 复制代码
    class Test {
    public:
    	int num;
    	string text;
    	static const int value = 1;
    };
    
    int main() {
    	const Test obj;
    	// 带有括号的表达式
    	decltype(obj.num) a = 0;		// 规则一 a: int
    	decltype((obj.num)) b = a;		// 规则三 b: const int&
    
    	// 加法表达式
    	int n = 0, m = 0;
    	decltype(n + m) c = 0;			// 规则一 c: int 表达式n+m不能被取地址,是个右值
    	decltype(n = n + m) d = n;		// 规则三 d: int&  n+m保存到n中,此时n是一个左值
    	return 0;
    }

2.2 decltype的应用

decltype 的应用多出现在泛型编程中,比如我们编写一个类模板,在里边添加遍历容器的函数,操作如下:

cpp 复制代码
template<class T>
class Container {
public:
	void print(T& t) {
		for (m_it = t.begin(); m_it != t.end(); m_it++) {
			cout << "valu: " << *m_it << endl;
		}
	}
private:
	// T::iterator m_it;			 // error 这里不能确定迭代器类型
	decltype(T().begin()) m_it;		
};

int main() {
	list<int>ls1{ 1,2,3,4,5,6,7 };
	const list<int> ls2{ 1,2,3,4,5,6,7 };	//常量容器 const_iterator
	Container<list<int>>c1;
	Container<const list<int>>c2;
	c1.print(ls1);
	c2.print(ls2);
	return 0;
}

在程序第10行报错,迭代器有两种类型:只读(T::const_iterator)读写(T::iterator),我们虽然能知道 T 的类型(属于什么容器的迭代器),但是不知道迭代器的类型,所以会报错。有了decltype就可以完美的解决这个问题了,当T是一个非 const 容器得到一个 T::iterator,当 T 是一个 const 容器时就会得到一个 T::const_iterator

3. 返回类型后置

在泛型编程中,可能需要通过参数的运算来得到返回值的类型,比如下面这个场景:

cpp 复制代码
//R->返回值类型, T->参数1类型, U->参数2类型
template<typename R,typename T,typename U>
R add(T t, U u) {
	return t + u;
}

int main() {
	int x = 520;
	double y = 13.14;
	auto z = add<decltype(x + y), int, double>(x, y);
	cout << "z = " << z << endl;
	return 0;
}

我们在调用函数 add 时,显示指定了返回值类型 decltype(x + y),但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。因此,如果要想解决这个问题就得直接在 add 函数身上做文章,先来看第一种写法:

cpp 复制代码
template<typename R,typename T,typename U>
decltype(t+u) add(T t, U u) {
	return t + u;
}

但是问题又来了,编译器这几行代码报错,因为 decltype 中的 tu 都是形参,直接这样写相当于变量还没有定义就直接用上了,这时候变量还不存在。在C++11中增加了返回类型后置语法,说明白一点就是将 decltype 和 auto 结合起来完成返回类型的推导,其语法格式如下:

cpp 复制代码
// 符号 -> 后边跟随的是函数返回值的类型
auto func(参数1, 参数2, ...) -> decltype(参数表达式)

通过对上述返回类型后置语法代码的分析,得到结论:auto会追踪 decltype() 推导出的类型。因此上边的add()函数可以做如下的修改:

cpp 复制代码
//R->返回值类型, T->参数1类型, U->参数2类型
template<typename T,typename U>
auto add(T t, U u) -> decltype(t + u)
{
	return t + u;
}

返回类型后置的好处在于,在指定返回值类型时就可以使用函数形参。

相关推荐
我是华为OD~HR~栗栗呀2 小时前
华为OD-C面经-23届学院哦
java·c++·python·华为od·华为·面试
xiaozi41202 小时前
Ruey S. Tsay《时间序列分析》Python实现笔记:综合与应用
开发语言·笔记·python·机器学习
西贝爱学习2 小时前
Visual Studio下载地址,vs2022安装程序
c++
天赐学c语言2 小时前
12.5 - 二叉树的最近公共祖先 && 构造函数和析构函数可以是虚函数吗
c++·二叉树·虚函数
wearegogog1232 小时前
DEA模型MATLAB实现(CCR、BCC、超效率)
开发语言·算法·matlab
闻缺陷则喜何志丹2 小时前
【计算几何】P2785 物理1(phsic1)- 磁通量|普及+
c++·数学·计算几何
郝学胜-神的一滴2 小时前
Linux定时器编程:深入理解setitimer函数
linux·服务器·开发语言·c++·程序人生
cici158742 小时前
基于反向传播算法实现手写数字识别的MATLAB实现
开发语言·算法·matlab
小鱼小鱼.oO2 小时前
C++ 算法基础知识
c++·算法·哈希算法