为何返回成员的副本而非引用?C++值类别与decltype(auto)解析
根据cppreference的说明,左值对象的成员同样是左值:
以下表达式为左值表达式:
a.m(对象成员表达式),除非m是成员枚举器、非静态成员函数,或者a是右值且m是对象类型的非静态数据成员;
我们来看一个示例程序:自定义类Test是int的包装类,添加了各类构造、赋值的打印语句;另有模板函数get,接受T的万能引用,用decltype(auto)返回其成员。
#include <iostream> struct Test { int member; explicit Test(int member): member(member) { std::cout << "Ctor" << std::endl; } Test(const Test& other): member(other.member) { std::cout << "Copy ctor" << std::endl; } Test(Test&& other) noexcept: member(other.member) { std::cout << "Move ctor" << std::endl; } Test& operator=(const Test& other) { member = other.member; std::cout << "Copy =" << std::endl; return *this; } Test& operator=(Test&& other) { member = other.member; std::cout << "Move =" << std::endl; return *this; } ~Test() { std::cout << "Dtor" << std::endl; } }; template <typename T> decltype(auto) get(T&& obj) { std::cout << "Is obj lvalue ref? " << std::is_lvalue_reference_v<decltype(std::forward<T>(obj))> << std::endl; return std::forward<T>(obj).member; } int main() { Test test{1234}; auto&& ret = get(test); ret = 5678; std::cout << "test.member is equal to " << test.member << std::endl; return 0; }
我对decltype(auto)的推导规则理解如下:
- 对于
prvalue,推导为其类型 - 对于
lvalue,推导为引用 - 对于
xvalue,推导为右值引用
但程序输出如下:
Ctor Is obj lvalue ref? 1 test.member is equal to 1234 Dtor
main函数中的赋值ret = 5678并未改变test对象的成员值,说明返回的是成员副本,这和我的理解不符,这是为什么?
问题出在decltype(auto)的推导细节上:decltype对表达式的推导规则分两种情况——
- 若表达式是无括号的标识符(比如直接访问的成员变量
member),decltype会直接取该标识符的声明类型; - 若表达式是带括号的复合表达式,
decltype才会根据表达式的值类别推导类型。
你的return std::forward<T>(obj).member;语句中,std::forward<T>(obj).member是无括号的成员访问表达式,属于标识符范畴,因此decltype(auto)会直接推导为member的声明类型int,而非左值对应的int&。这就导致函数返回的是原成员的副本,而非引用,所以对ret的赋值不会影响原对象的test.member。
只需要给返回的成员表达式加上括号,让decltype(auto)根据值类别推导:
return (std::forward<T>(obj).member);
修改后,(std::forward<T>(obj).member)是带括号的左值表达式,decltype(auto)会推导为int&,函数返回原成员的引用。此时程序输出会变为:
Ctor Is obj lvalue ref? 1 test.member is equal to 5678 Dtor
总结:decltype(auto)对无括号的成员/变量标识符,会直接取其声明的非引用类型;只有用括号包裹表达式,才会遵循值类别推导引用类型,这是容易被忽略的细节。
内容的提问来源于stack exchange,提问作者Mati

