当前位置: 首页 > 工具软件 > Understand > 使用案例 >

Item 3: Understand decltype

司英彦
2023-12-01

这次是对 Effective Modern C++ Item3 的学习笔记。

给定一个名字或者表达式,decltype 返回名字或者表达式的类型。大多数情况下,decltype 都能返回你所期望的结果,但也有一些特殊情况。

下面给出一些典型场景下,decltype 的返回结果,正如你所期望的那样:

const int i = 0; // decltype(i) is const int

bool f(const Widget& w); // decltype(w) is const Widget&
                         // decltype(f) is bool(const Widget&)
                         
struct Point {
int x, y;   // decltype(Point::x) is int
};          // decltype(Point::y) is int

Widget w; // decltype(w) is Widget

if (f(w)) ... // decltype(f(w)) is bool

template<typename T>    // simplified version of std::vector
class vector {
public:
...
T& operator[](std::size_t index);
...
};

vector<int> v;  // decltype(v) is vector<int>
...
if (v[0] == 0) ...   // decltype(v[0]) is int&

在C++11中,可能 decltype 基本的应用就是申明函数模板,它的返回值类型取决于其参数类型。例如,我们有这样一个函数,传入一个容器和下标,期望在函数末尾返回下标指向的值,并且保留返回值的类型。由于容器的 operatpr[] 通常返回一个 T& (但是,std::vector<bool>,opertor[] 返回的不是一个 bool&,这个我们在 Item6 中再讨论),我们期望在函数末尾返回的也是引用类型,以便于我们对其进一步赋值。使用 decltype 达成这样的目标:

template<typename Container, typename Index>  // works, but requires refinement
auto authAndAccess(Container& c, Index i)
-> decltype(c[i])
{
  authenticateUser();
  return c[i];
}

这里使用了拖尾返回值类型的语法,也即在箭头后声明返回值类型,这里可以返回我们期望的引用类型。

C++14可以省略拖尾返回值的声明,这意味着 auto 需要通过编译器根据函数的实现进行类型推导得到:

template<typename Container, typename Index>   // C++14; not quite correct
auto authAndAccess(Container& c, Index i)
{
  authenticateUser();
  return c[i];    // return type deduced from c[i]
}

但是 Item1Item2 的推导规则告诉我们,在模板类型推导中,表达式初始化的引用属性将被忽略。考虑下面的代码片段:

std::deque<int> d;    
...
authAndAccess(d, 5) = 10;   // authenticate user, return d[5], then assign 10 to it; this won't compile!

这里,d[5] 是一个 int&,但是根据 auto的类型推导,authAndAccess 函数将会返回一个 int 类型,作为函数返回值,即一个 int 类型的值,这是一个右值,那么对其进行赋值是非法的。

为了得到我们期望的结果,也即返回引用类型,我们需要使用 decltype 的类型推导:decltype(auto),auto 表示类型需要推导,decltype 表示使用 decltype 规则进行推导,我们修改函数实现如下:

template<typename Container, typename Index>   // C++14; works, but still requires refinement
decltype(auto) authAndAccess(Container& c, Index i)
{
  authenticateUser();
  return c[i];
}

这里,authAndAccess 将真正返回 c[i] 的类型,也即 T&

decltype(auto) 不限于用在函数返回值,也可以用在变量申明,例如:

Widget w;
const Widget& cw = w;
auto myWidget1 = cw;    // auto type deduction: myWidget1's type is Widget

decltype(auto) myWidget2 = cw;   // decltype type deduction: myWidget2's type is const Widget&

还有两点需要进一步讨论下,第一点是上面提到的 authAndAccess 还可以进一步优化的事情。让我们再看下 authAndAccess 的声明:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

这里,Container 传参是一个 lvalue-reference-to-non-const,返回值的类型是可以被修改的,这也就意味着我们不能传右值 containers,右值不能绑定到左值引用(除非是 lvalue-references-to-const )。

一个比较好的解决方案是万能引用!如下:

template<typename Container, typename Index>   
decltype(auto) authAndAccess(Container&& c, Index i);    // c is now a universal reference

在这个模板中,既可以传左值,也可以传右值。但是这里有一个不好地方上是:值传递会造成不必要的拷贝。

为了遵循 Item25 中的警告,我们需要修改下如下:

template<typename Container, typename Index>  // final C++14 version
decltype(auto)
authAndAccess(Container&& c, Index i)
{
  authenticateUser();
  return std::forward<Container>(c)[i];
}

以上需要 C++14 的编译器,C++11 版本的写法:

template<typename Container, typename Index>   // final C++11 version
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
  authenticateUser();
  return std::forward<Container>(c)[i];
}

另一问题是开头说的少数场景下,decltype 可能返回不是你所预期的结果。对于一个左值表达式 expr,如果 expr 的类型是 Tdecltype(expr) 返回的是 T&

int x = 0;

decltype(x)int。但是对于 (x) 是一个左值表达式,则decltype((x)) 得到 int&

decltype(auto) f1()
{
  int x = 0;
  ...
  return x;    // decltype(x) is int, so f1 returns int
}

decltype(auto) f2()
{
  int x = 0;
  ...
  return (x);    // decltype((x)) is int&, so f2 returns int&
}

f2f1 的返回值不同,f2 返回了一个局部变量的引用,这是相当危险的。

因此,在使用 decltype 的时候,建议使用 Item4 中的方法进行检查,确保得到你期望的类型推导。

最后,总结如下:

  • decltype几乎总是能够得出变量或者表达式的类型。
  • 对于类型为 T 的左值表达式,而不是名字,decltype 基本上总是输出 T&
  • C++14支持 delctype(auto),像是 auto,能够自动从初始化列表中推断出类型,但它使用的是decltype 的推断规则。
 类似资料:

相关阅读

相关文章

相关问答