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

来自:网络
时间:2023-07-24
阅读:
目录

引子

在编程过程中,有时我们需要根据表达式的类型来声明变量,尤其是在涉及模板编程和泛型编程时,经常会遇到这样的问题:(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,希望本文可以对大家有所帮助。

返回顶部
顶部