详解C++17中的decltype类型推导

 更新时间:2023年06月26日 09:58:54   作者:@一鸣惊人  
这篇文章主要介绍了C++17中的decltype类型推导,本文从泛型编程中经常会遇到2个常见问题入手,循序渐进的分析了从C++11开始引入的关键字decltype,需要的朋友可以参考下

引子

在编程过程中,有时我们需要根据表达式的类型来声明变量,尤其是在涉及模板编程和泛型编程时,经常会遇到这样的问题:(1)、有些泛型类型由模板参数决定,但是却很难或根本无法表示;(2)、需要在编译时确定变量的类型。

除此之外,我们知道auto在自动类型推导时,会忽略类型的修饰符。如此会导致auto推导的类型会与原表达式的类型存在不一致问题。

为了更好的解决这些问题,从C++11标准开始,C++引入了decltype关键字,其作用是让编译器在编译时识别表达式的类型,方便的的进行类型推导,同时也解决泛型编程和模板编程中变量类型表示的问题。

标准演进

decltype是declare type的缩写。C++11标准引入了decltype的核心功能和推导规则,C++11以后的各标准都本别对C++11自定的规则进行扩容和改进。具体演进过程如下所示:

  • C++11:引入关键字,并引入decltype的核心功能,用于根据表达式推导出变量的类型;
  • C++14:引入两个重要改进
  • 引入decltype(auto)语法,此语法可用于函数返回值类型的推导。基于decltype(auto)语法,函数的返回值类型可通过函数体的返回值表达式来推导,从而简化函数返回值类型的声明。
  • 放宽了对不完整类型的限制:在 C++11 中,如果 decltype 推导的表达式结果是一个不完整类型,那么会导致编译错误。而在 C++14 中,对不完整类型的处理更加宽松,允许使用decltype 推导不完整类型的变量。
  • C++17:decltype(atuo)支持非类型模板形参占位符。

C++11

引入关键字,并引入decltype的核心功能,用于根据表达式推导出变量的类型;当使用decltype(e) 推导表达式 e(类型为T)的类型时,C++11标准定义decltype的推导规则如下:

  • 如果是一个未加括号的标识符表达式或类成员访问,那么decltype(e)的推导结果为e类型T;假如不存在这样的实体或e是一组重载函数,那么decltype(e)无法推导。而且推导过程const/volatile 限定符会被忽略;
  • 如果e是一个可调用对象,那么decltype(e)推导为可调用对象返回值的类型;
  • 如果e是一个左值,decltype(e)推导为T&。const/volatile 限定符不能忽略;
  • 如果e是一个将亡值,decltype(e)推导为T&&,const/volatile 限定符不可忽略;
  • 如decltype(e)无法命中上述4情况,decltype(e)将会推导为e的类型T;

为了让大家更形象的理解这5条规则,下面我们通过一些示例来说明这五条推导规则。

示例 1: 未加括号标识符表达式

int x = 42;
decltype(x) y; // 推导结果是 int,满足第1条规则

示例 2: 加括号的标识符表达式

int x = 42;
decltype((x)) y = x; // 推导结果是 int&,满足第三条规则

示例3:未加括号的类成员访问

struct MyClass {
    int member;
};
const MyClass obj;
decltype(obj.member) result = obj.member; // 推导结果是 int, 忽略const/volatile 限定符,满足第1条规则

示例4:加括号的类成员访问

struct MyClass {
    int member;
};
const MyClass obj;
decltype((bj.member)) result = obj.member; // 推导结果是 const int&, const/volatile 限定符不能忽略,满足第3条规则

示例 5: 可调用对象表达式

int add(int a, int b)
{
    return a + b;
}
decltype(add(1, 2)) result; // 推导结果是 int,满足第2条规则

示例 6: 将亡值

int x = 42;
decltype(std::move(x)) result = std::move(x); // 推导结果为int&&,std::move(x) 为将亡值

示例 7: 右值表达式

int x = 42;
decltype(x + 1) result; // 推导结果是 int(右值表达式 x + 1 的类型是 int)

示例8:右值引用变量

int&& i = 500;
decltype(i) x2;           // x2的类型是int&&,满足第5条

C++14

C++14主要引进了两个重要改进,他们分别是:放宽对不完整类型的限制;引入decltype(auto)语法。

放宽对不完整类型的限制

C++11标准要求decltype在使用时,推导的表达式必须是完整类型。如果decltype推导的表达式是一个不完整类型,例如某个类的声明但尚未定义,那么会导致编译错误。C++14对这个限制进行了放宽,允许使用decltype推导不完整类型的变量。这使得编写一些特定的模板代码更加方便,因为在某些情况下,可能需要推导出不完整类型。

但是,虽然C++14放宽了对不完整类型的限制,但仍然要求推导的表达式在使用时必须是可见的,即需要在推导时至少对类型进行了前向声明。否则,将会导致编译错误。

以下是一个示例,演示如何在泛型编程中使用 decltype 推导不完整类型:

template <typename T>
struct Container
{
    using ValueType = decltype(*std::declval<T>()); // 使用 decltype 推导不完整类型
    // 其他成员和函数...
};
int main()
{
    Container<std::vector<int>> container;
    using ValueType = typename decltype(container)::ValueType; // 推导结果为 int&
    return 0;
}

decltype(auto)

除了放宽对不完整类型的限制,C++14还有一个特色就是decltype(auto)decltype(auto)作用是告诉编译器auto的推导规则遵循decltype而非auto。不过有一点需要注意就是decltype(auto)必须单独声明,不能与其他相结合。所以下述声明是不合法的:decltype(auto)*const decltype(auto), volatile decltype(auto)

decltype(auto) 的推导规则如下:

  • 如果初始化表达式是一个标识符表达式,那么decltype(auto)推导为表达式的类型(const/volatile 限定符和引用修饰符不能忽略);
  • 如果初始化表达式是一个函数调用表达式,那么decltype(auto)推导为函数调用表达式的返回类型;
  • 如果初始化表达式是一个左值表达式(如变量名、数组名、成员访问等),那么decltype(auto)推导为对应左值类型的引用类型(const/volatile 限定符和引用修饰符不能忽略)。
  • 如果初始化表达式是一个右值表达式(如字面值、临时对象、表达式的结果等),那么decltype(auto)推导为对应右值的类型(const/volatile 限定符和引用修饰符不能忽略)。
  • 如果初始化表达式是一个将亡值(如移动赋值),那么decltype(auto)推导为对应类型的右值引用

示例 1:标识符表达式

int x = 42;
decltype(auto) y = x; // 推导结果是 int(x 的类型)

示例 2:函数调用表达式

int add(int a, int b)
{
    return a + b;
}
decltype(auto) result = add(1, 2); // 推导结果是 int(add 函数返回类型)

示例 3:左值表达式

const int x = 42;
decltype(auto) ref = (x); // 推导结果是 const int&(x 的引用类型)

示例4:右值表达式

decltype(auto) x2 = 50; // 推导结果是 int

示例4:将亡值

int x2 = 50;
decltype(auto) x3 = std::move(x2); // 推导结果为int&&

除了变量类型推导以外,在C++14中引入了decltype(auto)作为一种返回类型的语法。它用于在函数声明中指定返回类型,该返回类型将从函数体中的表达式推导而来。

为了更好的理解decltype(auto)作为一种返回类型的语法,我们参考下面三种函数返回类型自动推导定义方式。

第一种: C++14 基于auto新特性返回值类型自动推导

template<typename Container, typename Index>
auto accessOrUpdate(Container& c, Index i) { 
  return c[i];  // 返回类推导为c[i]的类型,而且会异常引用限制      
}
std::vector<int> v{1,2,3,4,5};
accessOrUpdate(v,2) = 10;      // 编译错误,不允许赋值

第二种:C++14 基于auto和decltype实现返回值类型推导

template <typename Container, typename Index>
auto accessOrUpdate(Container &c, Index i) -> decltype(c[i]) {
  return c[i];
}
std::vector<int> v{1,2,3,4,5};
accessOrUpdate(v,2) = 10;

第三种:C++14 decltype(auto)实现返回值类型推导

template <typename Container, typename Index>
decltype(auto) accessOrUpdate(Container &c, Index i) {
  return c[i];
}
std::vector<int> v{1,2,3,4,5};
accessOrUpdate(v,2) = 10;

对比上述三种函数返回值类型推导,decltype(auto)可让编译器根据表达式的类型自动推导函数的返回类型,而不需要显式地指定返回类型。这种方式可简化代码,而且推导更加灵活。

C++17

为与auto交相辉映,C++17开始支持decltype(auto)非类型模板。但是需特别注意的是在C++17标准中,非类型模板参数类型必须是整理类型(int, short, long等),枚举类型,指针类型,左值引用类型和std::nullptr_t,而自定义类型,浮点数和字符串则不允许作非类型模板参数。

template<decltype(auto) n>  // C++17 decltype(auto)形参声明
auto f() -> std::pair<decltype(n), decltype(n)> // auto 不能从花括号初始化器列表推导
{
    return {n, n};
}
f<5>();      // n为int
f<(5)>();    // n为int&
f<'a'>();    // n为char
f<('a')>();  // n为char&
f<1.0>();    // 编译失败double不能作为模板参数,double不允许做非类型模板参数。

C++20允许字面量类类型作为非类型模板参数。例如在C++20之前,下述代码无法编译通过,而在C++20中则可以编译通过。

class A {};
template <A a>
class B {};
A a;
B<a> b;  // C++20 前编译失败,C++20 可以编译成功。

总结

本文从泛型编程中经常会遇到2个常见问题入手,循序渐进的分析了从C++11开始引入的关键字decltype,希望本文可以对大家有所帮助。

相关文章

  • Dev-C++无法使用bits/stdc++.h问题及解决

    Dev-C++无法使用bits/stdc++.h问题及解决

    这篇文章主要介绍了Dev-C++无法使用bits/stdc++.h问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • C语言实现最简单的剪刀石头布小游戏示例

    C语言实现最简单的剪刀石头布小游戏示例

    这篇文章主要介绍了C语言实现最简单的剪刀石头布小游戏,涉及C语言数组、随机数与数值运算等相关操作技巧,需要的朋友可以参考下
    2017-09-09
  • 剖析C++中的常量表达式与省略号的相关作用

    剖析C++中的常量表达式与省略号的相关作用

    这篇文章主要介绍了C++中的常量表达式与省略号的相关作用,以及表达式中的可变参数模板示例,需要的朋友可以参考下
    2016-01-01
  • C++实现随机生成迷宫地牢

    C++实现随机生成迷宫地牢

    这篇文章主要介绍了C++实现随机生成迷宫地牢的相关资料及代码分享,推荐给大家,有需要的小伙伴可以参考下。
    2015-03-03
  • C++数据结构与算法之判断一个链表是否为回文结构的方法

    C++数据结构与算法之判断一个链表是否为回文结构的方法

    这篇文章主要介绍了C++数据结构与算法之判断一个链表是否为回文结构的方法,结合实例形式分析了回文结构并结合实例给出了C++判断回文的操作技巧,需要的朋友可以参考下
    2017-05-05
  • 如何利用C语言输出3D立体感心形图详解

    如何利用C语言输出3D立体感心形图详解

    其实我们在程序中也有很多乐趣的,只是很多人不善于发现,这篇文章主要给大家介绍了关于C语言输出3D立体感心形图的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2021-12-12
  • 深入解析C++11 lambda表达式/包装器/线程库

    深入解析C++11 lambda表达式/包装器/线程库

    这篇文章主要介绍了C++11 lambda表达式/包装器/线程库的相关知识,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05
  • C++实现LeetCode(133.克隆无向图)

    C++实现LeetCode(133.克隆无向图)

    这篇文章主要介绍了C++实现LeetCode(133.克隆无向图),本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • c语言:基于函数指针的两个示例分析

    c语言:基于函数指针的两个示例分析

    本篇文章是对c语言中函数指针的两个示例做了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C++ decltype用法举例说明

    C++ decltype用法举例说明

    decltype是C++11添加的一个新的关键字,目的是选择并返回操作数的数据类型,重要的是,在此过程中编译器分析表达式并得到它的类型,却不实际计算表达式的值,今天通过本文给大家介绍C++ decltype用法,通过实例代码给大家介绍的非常详细,需要的朋友参考下吧
    2021-07-07

最新评论