CMake:设置语言标准(一)

C++11新特性详解:auto、decltype、返回值类型后置与右值引用

导言

编程语言有不同的标准,启用新标准是通过设置适当的编译器标志来实现的。首先让我们来了解一下关于C++ 11的相关特性。

C++标准历史

  • 1998 年,C++ 标准委员会发布了第一版 C++ 标准,并将其命名为 C++ 98 标准
  • 2011 年,新的 C++ 11 标准诞生,用于取代 C++ 98 标准。此标准还有一个别名,为C++ 0x
  • 2014 年,C++ 14 标准发布,该标准库对 C++ 11 标准库做了更优的修改和更新
  • 2017 年底,C++ 17 标准正式颁布

C++11版本特性介绍

C++ 11 标准之前,C++ 标准委员会还在 2003 年对 C++ 98 标准做了一次修改(称为 C++ 03 标准),但由于仅仅修复了一些 C++ 98 标准中存在的漏洞,并未修改核心语法,因此人们习惯将这次修订和 C++ 98 合称为 C++98/03 标准。

以上 3 个标准中,C++ 11 标准无疑是颠覆性的,该标准在 C++ 98 的基础上修正了约 600C++ 语言中存在的缺陷,同时添加了约 140 个新特性,这些更新使得 C++ 语言焕然一新

类型推导之auto和decltype

C++11 之前的版本中,定义变量或者声明变量之前都必须指明它的类型,比如 intchar 等。C++11 使用 auto 关键字来支持自动类型推导。

在之前的 C++ 版本中,auto 用来指明变量的存储类型,它和 static 是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,这使得 auto 的存在变得非常鸡肋。

C++ 11 赋予 auto 新的含义,用它来做自动类型推导。即,使用 auto 关键字后,编译器会在编译期间自动推导出变量的类型。

注意:

  • auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
  • 使用 auto 类型推导的变量必须马上初始化,因为 autoC++11 中只是占位符,并非如 int 一样的真正的类型声明。

auto与const的结合使用

int x = 0;
// n 为const int,auto 被推导为int
const auto n = x;
// f为const int, auto 被推导为int(const属性被抛弃)
auto f = n;
// r1为const int &类型,auto被推导为int
const auto &r1 = x;
// r1为const int&类型,auto 被推导为const int 类型
auto &r2 = r1;

autoconst 结合的用法:

  • 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
  • 当类型为引用时,auto 的推导结果将保留表达式的 const 属性。
    auto的限制:
  • 使用auto时必须对变量进行初始化
  • auto不能作为函数的形参
  • auto 不能作用于类的非静态成员变量
  • auto 关键字不能定义数组
  • auto 不能作用于模板参数

decltypeC++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。decltype 是declare type的缩写,译为声明类型

auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。

auto var_name = value;
decltype(exp) var_name = value;

其中,var_name 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。

auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。

auto 要求变量必须初始化,而 decltype 不要求。

exp 就是一个普通的表达式,它可以是任意复杂的形式,但是必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。

int a = 0;
// b 被推导成了 int
decltype(a) b = 1;  
// x 被推导成了 double
decltype(10.8) x = 5.5;  
// y 被推导成了 double
decltype(x + 100) y;  

decltype 推导规则

  • 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致。
  • 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  • 如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&

注意
左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据;
右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据。
有一种很简单的方法来区分左值右值,对表达式取地址,如果编译器不报错就为左值,否则为右值。

auto与decltype对 cv 限定符的处理
cv 限定符是 const 和 volatile 关键字的统称:

  • const 关键字用来表示数据是只读的,也就是不能被修改
  • volatileconst 是相反的,它用来表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取

在推导变量类型时,auto 和 decltype 对 cv 限制符的处理是不一样的。decltype 会保留 cv 限定符,而 auto 有可能会去掉 cv 限定符。其原理见auto与const的结合使用

auto与decltype对 引用的处理
当表达式的类型为引用时,auto 和 decltype 的推导规则也不一样;decltype 会保留引用类型,而 auto 会抛弃引用类型,直接推导出它的原始类型。

C++返回值类型后置

在泛型编程中,如果需要通过参数的运算来得到返回值的类型:

template <typename R, typename T, typename U>
R Add(T t, U u)
{
   
   
    return t+u;
}

int main() {
   
   
  int a = 1;
  float b = 2.0f;
  auto c = Add<decltype(a + b)>(a + b);
  return 0;
}

以上代码是因为我们并不关心a + b的类型是什么,因此只需要通过decltype(a + b)直接得到返回值类型即可。

上述使用过程十分不方便,因为外部其实并不知道参数之间应该如何运算,只有Add函数知道返回值应该如何推导。

在函数定义上直接通过decltype获取返回值:

template <typename T, typename U>
decltype(T() + U()) add(T t, U u) {
   
   
    return t + u;
}

考虑到 T、U 可能是没有无参构造函数的类,正确的写法如下:

template <typename T, typename U>
decltype((*(T*)0) + (*(U*)0)) add(T t, U u) {
   
   
    return t + u;
}

上述代码虽然成功地使用 decltype 完成了返回值的推导,但写法过于晦涩,会大大增加decltype 在返回值类型推导上的使用难度并降低代码的可读性。

因此,在 C++11 中增加了返回类型后置语法,将 decltypeauto 结合起来完成返回值类型的推导。

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u){
   
   
    return t + u;
}

对模板实例化中连续尖括号>>的改进

C++98/03 的泛型编程中,模板实例化过程中,连续两个右尖括号(>>)会被编译器解释成右移操作符,而不是模板参数表的结束。

template <typename T>
struct Foo{
   
   
  typedef T type;
};

template <typename T>
class A{
   
   
  // ...
};

int main(){
   
   
  //编译出错
  Foo<A<int>>::type xx;  
  return 0;
}

上述代码使用 gcc 编译时,会得到如下错误提示:

error: ‘>>’ should be ‘>>’ within a nested template argument list Foo<A>::type xx;

意思就是,Foo<A<int>>这种写法是不被支持的,要写成这样Foo<A<int> >(注意两个右尖括号之间的空格)。

这种限制是很没有必要的。因为在 C++ 的各种成对括号中,目前只有右尖括号连续写两个会出现这种二义性。static_castreinterpret_castC++ 标准转换运算符,都是使用<>来获得待转换类型(type-id)的。若这个 type-id 本身是一个模板,用起来会很不方便。

C++11 标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出>>是一个右移操作符还是模板参数表的结束标记。

注意:上述这种自动化的处理在某些时候会与老标准不兼容:

template <int N>
struct Foo{
   
   
  // ...
};

int main() {
   
   
  // 解决方案:
  // Foo<(100 >> 2)> xx;
  Foo<100 >> 2> xx; 
  return 0;
}

C++98/03 的编译器中编译是没问题的,但 C++11 的编译器会显示:

error: expected unqualif?ied-id before ‘>’ token Foo<100 >> 2> xx;

使用using定义别名(替代typedef)

C++可以使用typedef重定义一个类型,被重定义的类型不一定是一个新的类型,也有可能仅仅是原有类型取了一个新的名字。使用typedef重定义类型是很方便的,但它也有一些限制,如无法重定义一个模板等。

template<typename T>
using str_map_t = std::map<std::string, T>;
// ...
str_map_t<int>map_1;

实际上,using的别名语法覆盖了typedef的全部功能。

// 重定义unsigned int
typedef unsigned int uint_t;
using uint_t = unsigned int;
// 重定义std::map
typedef std::map<std::string, int> map_int_t;
using map_int_t = std::map<std::string, int>;

// 重定义模板
// C++98/03 
template <typename T>
struct func_t{
   
   
    typedef void (*type)(T, T);
};
// 使用 func_t 模板
func_t<int>::type xx_1;
// C++11 
template <typename T>
using func_t = void (*)(T, T);
// 使用 func_t 模板
func_t<int> xx_2;

从示例中可以看出,通过 using 定义模板别名的语法,只是在普通类型别名语法的基础上增加 template 的参数列表。使用 using 可以轻松地创建一个新的模板别名,而不需要像 C++98/03 那样使用烦琐的外敷模板。

支持函数模板的默认参数

C++98/03 标准中,类模板可以有默认的模板参数:

template <typename T, typename U = int, U N = 0>
struct Foo{
   
   
    // ...
};

但是不支持函数的默认模板参数:

// error in C++98/03: default template arguments
template <typename T = int>  
void func(){
   
   
    // ...
}

现在这一限制在 C++11 中被解除了。上面的 func 函数在 C++11 中可以直接使用:

int main(void){
   
   
	//T = int
    func();   
    return 0;
}

函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同,它没有必须写在参数表最后的限制。甚至于,根据实际场景中函数模板被调用的情形,编译器还可以自行推导出部分模板参数的类型。即当默认模板参数和编译器自行推导出模板参数类型的能力一起结合使用时,代码的书写将变得异常灵活。我们可以指定函数中的一部分模板参数采用默认参数,而另一部分使用自动推导:

template <typename R = int, typename U>
R func(U val){
   
   
    return val;
}

int main(){
   
   
	// R=int, U=int
    func(97);               
	// R=char, U=int
    func<char>(97);         
	// R=double, U=int
    func<double, int>(97);  
    return 0;
}

当默认模板参数和自行推导的模板参数同时使用时,若无法推导出函数模板参数的类型,编译器会选择使用默认模板参数;如果模板参数无法推导出来,又未设置其默认值,则编译器直接报错。

template <typename T, typename U = double>
void func(T val1 = 0, U val2 = 0) {
   
   
    //...
}

int main() {
   
   
	// T=char, U=double
    func('c'); 
	// 编译报错
    func();    
    return 0;
}

在函数模板和类模板中使用可变参数

可变参数,指的是参数的个数和类型都可以是任意的。

对于函数参数而言,C++ 一直都支持为函数设置可变参数,最典型的代表就是 printf() 函数,它的语法格式为:

int printf ( const char 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值