Effective Modern C++ 条款3 理解decltype

本文详细解析了C++中的decltype工具,解释了它如何确定变量或表达式的类型,并介绍了decltype(auto)在C++14中的使用,包括如何正确声明模板函数的返回类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款3 理解decltype

decltype是一个诡异的工具,指定一个变量名或者一个表达式,decltype会告诉你变量或者表达式的类型。通常,decltype告诉你的结果和你预期的一样,但是偶尔会让你很头疼,去查找资料或者去上网提问。


我们先来看一些不会出乎我们的意料的例子。相比于模板类型推断和auto类型推断(看条款1和条款2),

decltype几乎总是机械地返回变量或者表达式的确切类型

注意,decltype并没有进行类型推断:

decltype(27)  等同于 int

const int i = 0;   // decltype(i) 等同于const int

bool f(const Widget& w);  //decltype(i)等同于const Widget&
                    // decltype(f)等同于bool (const Widget&)

struct Point {
    int  x, y;      // decltype(Point::x)等同于int
};                            // decltype(Point::y)等同于int

Widget w;  // decltype(w)等同于Widget

if (f(w))...   // decltype(f(w))等同于bool

template <typename T>
class vector {
public:
...
T& operator[](std::size_t index);
...
};
vector<int> v;  // decltype(v)等同于vector<int>
...
if (v[0] == 0)...   // decltype(v[0)等同于int &

这些例子都没有出现奇怪的问题。


在c++11中,decltype最主要的用途可能是用来声明模板函数的返回值类型,而这些模板的返回值是取决如传进来的参数。例如,有一个函数的参数是一个容器(支持下标索引)和一个索引,在返回该索引处的值时要先验证用户,那么该函数的返回值类型应该与容器内元素的类型一致。

通常,容器的operator[]函数的返回值类型为T&,但在vector<bool>中的operator[]函数并不会返回bool&,而是返回一个新的对象,这种情况我们会在条款6中进行探讨。但是这里我们想说的是,容器的operator[]函数的返回值类型取决于不同的容器。

decltype很容易写出我们刚说的模板的函数,不过等下我们还会优化它:

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

函数名前的auto没有进行类型推断,这只是c++11的返回类型后置语法(trailing return type),函数的返回值类型只会取决于参数列表(“->” 之后的参数)。返回类型后置的好处是可以用函数的参数来说明返回值类型。在上面的例子中,我们用ci 来说明了函数的返回类型,如果我们在函数名之前直接用decltype(c[i])声明返回类型是错误的,因为此时的ci 还没有声明。

总的来说,authAndAccess函数返回的类型就是容器的operator[]函数的返回类型。


c++11可以推断出单一声明的lambda的返回类型(通过auto),而在c++14中,这个特性已经延伸到所有的lambda和所有的函数,包括那些复杂的声明。所以在c++14中,authAndAccess函数可以忽略掉返回类型后置,只留下auto,这样的话编译器会为return的值进行类型推断:

template <typename Container, typename Index>
auto authAndAccess(Container &c, Index i)
{
     authenticateUser();
     return c[i];  // 返回类型从c[i]推断
}

在条款2中我们已经说明,返回值类型用auto来声明时,编译器用的是模板参数类型推断的规则,而在这个例子中,这是有问题的。正如我们之前讨论,绝大多数容器的operator[]函数的返回类型为T&,但我们在条款1模板参数类型推断中,我们也说明了传进函数的参数的引用语义会被忽略。让我们考虑下面的代码:

std::deque<int> d;
...
authAndAccess(d, 5) = 10;  // 无法通过编译

在这里,返回值为d[5],d[5]的类型是int&,而在函数内的类型推断中,去掉了引用,所以返回类型是int,一个返回值语言的函数是一个右值,而在右值赋值是错误的,所以无法通过编译。

如果想让authAndAccess函数运行上面的代码,我们要用decltype作为函数返回类型。例如,说明函数的返回类型要和表达式c[i]的返回类型一致。c++的维护者预料到某些类型推断(auto或者模板推断)情况需要用到decltype类型返回的规则,所以在c++14中制定了decltype(auto)。这第一眼看过去就很矛盾,这组合的实际意思是:auto说明类型需要编译器推断,而decltype说明decltype的返回规则应用在类型推断上,因此我们改写我们的authAndAccess函数:

template <typename Container, typename Index>
decltype(auto)
authAndAccess(Container &c, Index i)
{`
    authenticateUser();  // C++14 下可以运行
    return c[i];            // 但还可以优化
}

现在authAndAccess函数可以正确的返回c[i]返回的类型,如果c[i]返回的是T&,那么authAndAccess返回的是T&,如果c[i]返回的是一个新的对象,那么authAndAccess返回的也是一个新的对象。

在网上找到一句话:

在你不知道返回值类型是个reference还是个value并且你想把返回的类型forward给别人的时候你就可以用decltype(auto).

当然decltype(auto)不只是可以用在函数返回值类型上,它也可以用在声明变量上:

Widget w;
const Widget &cw = w;
auto myWidget1 = cw; // auto类型推断,myWidget1类型为Widget
decltype(auto) myWidget2 = cw; // decltype原则推断
                               //myWidget2类型为const Widget &

我知道现在还有两件事困扰着你,一个是我提到的authAndAccess的优化,我现在就讲它。

让我们再看一次在c++14版本下的函数声明:

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

容器的参数是通过非const左值引用传递的,因为返回容器的元素是可以让用户修改的,但这也意味着无法传递右值容器给该函数,因为右值无法绑定到左值引用上(除非是const左值引用,但这里不适应)。

诚然,把一个右值容器传入authAndAccess中是一种边缘情况。右值容器将是一个生命期短暂的对象,离开了authAndAccess作用域就会被析构,这意味着返回的对于容器元素的引用(通常authAndAccess会返回引用)会变成空悬引用。所以要让authAndAccess传入右值容器变得有意义,那么用户只可以获取一份容器元素的拷贝,例如:
std::deque<std::string> makeStringDeque(); // 工厂函数
// 获取makeStringDeque返回容器中的第5个元素的拷贝
auto s = authAndAccess(makeStringDeque(), 5);

如果要让authAndAccess函数支持这样的用法意味着我们需要修改它的声明让它既可以接受左值,又可以接受右值。虽说重载也是可行的(一个声明接受左值,一个右值),但是这样我们需要维护两个函数。我们可以使用通用引用(universal reference)来避免重载,因为通用引用(universal reference)可以绑定左值和右值,具体原因见条款24。所以我们把anthAndAccess声明为:

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

在这个模板中,我们并不知道我们操作的容器的具体类型,同时我们也可以忽略我们使用的Index 的类型。在函数参数中对一个类型未知的对象使用值传递(pass-by-value)可能会因为不必要的拷贝降低性能,这种行为还会对对象产生限制(见条款41),并且还有可能被同事嘲笑。但是就索引Index 对象来说,标准库(参见std::string, std::vector, std::deque里的operator[]函数)里使用值传递好像是合情合理,所以我们在这里坚持使用值传递来给Index 对象赋值。

我们还需要更新模板的实现,根据条款25的劝告,我们使用std::forward来转发通用引用(universal reference):

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

std::forward就可以保存参数的左值或右值特性。

现在这个函数可以传任何类型的容器和索引,不过它需要一个c++14的编译器,如果你的编译器只支持c++11的话,那么你需要用c++11的版本。下面这份代码就是c++11的版本,效果与c++14版本的相同,不过是自己使用了返回类型后置说明了返回类型:

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

另一件困扰你的事是,就像我所说就是decltype几乎总是返回你期待的类型,它很少出乎我们意料。实话说,你不太可能会遇到这种规则的异常除非你是大型库的开发者。

为了理解透彻decltype的行为,你需要学习很多东西,很多东西很难理解以至于不应该在这里讨论,所以这样只看一个例子。

对一个名字使用decltype,会返回那个名字的具体类型。名字都属于左值表达式,但是这样并不会影响decltype的行为。decltype对于左值表达式的处理比名字的处理复杂很多,但是decltype确保会返回一个左值引用。这句话的意思是,如果一个左值表达式不同于名字,那么decltype会返回一个左值引用。

这种decltype的意义值得让我们留意。例如:
int x = 0;
x是一个名字,所以decltype(x)等同于int,但如果在加上括号(x),就不同于名字了,而c++规定(x)这样的括号加个名字也是左值,所以decltype((x))会返回int&。所以加个括号可能会改变decltype的类型。

在c++11中,这特性看起来有点奇怪,但是如果结合c++14的decltype(auto)后,看起来就是一个很顺其自然的特性,用来改变返回值类型:

decltype(auto) f1()
{
    int x = 0;
    ...
    return x;`   // decltype(x)是int,所以f1()返回int
}

decltype(auto) f2()
{
    int x = 0;
    ...
    return (x);  // decltype((x))是int&,所以f2()返回int&
}

你要注意的不仅仅是f1和f2的返回值类型不一样,还要注意f2返回一个局部引用!这会产生未定义行为。

这个问题主要是出现在使用decltype(auto),看似不起眼的细节会影响decltype(auto)推断的结果。为了让类型推断能如你期望的进行,你应该使用条款4里讲述的技术。


总结

虽说decltype有时会出乎我们的意料之外,但是dectltype大部分情况下都会返回我们期望的类型。特别是我们把decltype`应用在名字上,它只会返回名字的声明类型。

需要记住的3点:

  • decltype几乎总是不加修改的返回名字或者表达式的类型。
  • 一个类型为T左值表达式,除了名字外,decltype总是返回T&
  • C++14支持decltype(auto),行为是用decltype的规则来进行类型推断。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值