成员函数签名末尾的&(和号)代表什么意思?

30
我记不清是哪个讲座了,但最近我看了一些来自CppCon 2017的讲座,其中有人提到了一种附注的方式,即唯一的真正重载operator=的方法应该是这样的:
class test {
public:
    test& operator=(const test&) &;
};

他明确强调了尾随的&,但没有说明它的作用。
那么它到底是做什么的呢?

这是一种基于调用成员函数的对象的lvalue/rvalue类型来重载函数的方法。因此,同一类的2个对象(实例化)调用相同的函数(方法)名称时,实际上可以调用2个不同的函数实现,因为该函数可以根据调用该方法的对象是const lvalue(const &在func参数后面)、常规lvalue(&)、const rvalue(const &&)或常规rvalue(&&)进行重载。来源:这是我对此答案的总结:https://dev59.com/0FYN5IYBdhLWcg3wuaJx#47003980。 - Gabriel Staples
1
@GabrielStaples 更具体地说,关于不同的值类别,如果存在rvalue ref-qualifier重载函数,const和非const prvalues以及xvalues(因为xvalues绑定到rvalue引用)都会优先选择rvalue ref-qualifier重载函数。如果我们删除rvalue ref-qualifier重载函数但保留显式的lvalue ref-qualifier重载函数,则这些rvalue类别将都优先选择const &重载函数,因为rvalue可以用于初始化const lvalue引用。 - dfrib
2个回答

40

引用限定符 - C++11中引入的特性

引用限定符不是C++17的新功能(从问题标签可以看出),而是在C++11中引入的特性。

struct Foo
{
  void bar() const &  { std::cout << "const lvalue Foo\n"; }
  void bar()       &  { std::cout << "lvalue Foo\n"; }
  void bar() const && { std::cout << "const rvalue Foo\n"; }
  void bar()       && { std::cout << "rvalue Foo\n"; }
};

const Foo&& getFoo() { return std::move(Foo()); }

int main()
{
  const Foo c_foo;
  Foo foo;

  c_foo.bar();            // const lvalue Foo
  foo.bar();              // lvalue Foo
  getFoo().bar();         // [prvalue] const rvalue Foo
  Foo().bar();            // [prvalue] rvalue Foo

  // xvalues bind to rvalue references, and overload resolution
  // favours selecting the rvalue ref-qualifier overloads.
  std::move(c_foo).bar(); // [xvalue] const rvalue Foo
  std::move(foo).bar();   // [xvalue] rvalue Foo
}

请注意,rvalue 可以用于初始化 const lvalue 引用(从而扩展标识 rvalue 的对象的生命周期),这意味着如果我们从上面的示例中删除 rvalue ref-qualifier 重载,则示例中的 rvalue 值类别将都支持剩余的 const & 重载。
struct Foo
{
  void bar() const & { std::cout << "const lvalue Foo\n"; }
  void bar()       & { std::cout << "lvalue Foo\n"; }
};

const Foo&& getFoo() { return std::move(Foo()); }

int main()
{
  const Foo c_foo;
  Foo foo;

  // For all rvalue value categories overload resolution
  // now selects the 'const &' overload, as an rvalue may
  // be used to initialize a const lvalue reference.
  c_foo.bar();            // const lvalue Foo
  foo.bar();              // lvalue Foo
  getFoo().bar();         // const lvalue Foo
  Foo().bar();            // const lvalue Foo
  std::move(c_foo).bar(); // const lvalue Foo
  std::move(foo).bar();   // const lvalue Foo
}

请参见以下博客文章,简要介绍了此技术:

rvalues不能调用非const & 重载

为了可能解释您在CppCon演讲中提到的话语意图,

"... the only true way of overloading operator= ..."

我们查看 [over.match.funcs]/1, /4 & /5 [强调是我的]:

/1[over.match.funcs]的子条款描述了在每个上下文中提交给重载分辨率的候选函数集和参数列表。...

/4对于非静态成员函数,隐式对象参数的类型为

  • (4.1) — 函数声明没有引用限定符或者有 & 引用限定符时,“cv&”。

  • (4.2) — 函数声明带有 && 引用限定符时,“cv&&”。

其中T是函数成员所属的类,cv是成员函数声明上的限定符。

/5 ...对于没有引用限定符的非静态成员函数,还有一条额外规则适用:

  • (5.1) — 即使隐式对象参数没有被const限定,只要在所有其他方面上能将参数转换为隐式对象参数的类型,就可以将rvalue绑定到该参数上。[注释:这样的参数是rvalue并不影响隐式转换序列的排名。-结束备注]

根据上述/5,以下重载函数是可行的(省略了显式的 & 引用限定符):

struct test
{
    test& operator=(const test&) { return *this }
}

允许将值赋给r-value,例如:

int main()
{
    test t1;
    t1 = test(); // assign to l-value
    test() = t1; // assign to r-value
}

然而,如果我们明确声明带有 & 引用限定符的重载函数,[over.match.funcs]/5.1 不适用,并且只要我们不提供使用 && 引用限定符声明的重载函数,就不允许进行右值赋值操作。

struct test
{
    test& operator=(const test&) & { return *this; }
};

int main()
{
    test t1;
    t1 = test(); // assign to l-value
    test() = t1; // error [clang]: error: no viable overloaded '='
}

我不会就是否在声明自定义赋值运算符重载时明确包含& ref-qualifier发表任何意见,但是如果我敢猜测,那么我会猜测这样一个声明背后的意图是排除对to-r-value赋值的影响。

由于一个正确设计的赋值运算符应该永远不会是constconst T& operator=(const T&) const &没有多少意义),并且rvalue不能用于初始化非const lvalue引用,因此对于给定类型Toperator=一组仅包含T& operator=(const T&) &的重载将永远无法提供可行的重载,可以从被识别为rvalue值类别的T对象调用。


6
这就是为什么C++完全疯了。顺便说一句,回答很好。 - Gabriel Staples
3
我在学习C++的纯粹语法方面花费了比在C中多得多得多的时间。在C中,我学习了语法并努力理解架构,然后我就可以高效地工作了。但在C++中,我似乎从未学会所有的语法,所以“什么时候才能真正高效工作?”这个开放性问题总是一个不断变化的答案,而且总是难以达到。 - Gabriel Staples
1
@GabrielStaples 我完全同意“永远是C++新手”的感觉,特别是由于语言的许多部分正在快速发展。我尝试过Swift几年(一种非常好的语言),但出于某种原因,即使面临挑战,我仍坚持使用C++。也许我实际上觉得不断学习的情况相当有趣。但谁知道呢。我认为,只使用语言的子集(并且熟知该子集)就可以在生产力方面取得很大进步,但是即使达到这一点,门槛也可能相当高。 - dfrib
2
我之前没有听说过Candela,看起来真的很不错。他们位于瑞典西海岸(靠近斯德哥尔摩),而我位于东海岸(哥德堡),从事陆地交通方面的工作,而不是海上运输;在汽车行业进行安全关键的C++开发(实时嵌入式/POSIX环境中的AD/ADAS软件)。这通常意味着使用C++的严格子集,这也许是为什么我会去回答StackOverflow的问题和进行一些业余项目,以使用更大部分的语言,减少限制的原因 :) - dfrib
1
@GabrielStaples - 自从80年代末开始使用C++以来,我仍然有同样的感觉。随着越来越多的边缘情况的细节被添加到无尽的值类型、推导、范围、视图、时间等中,它更像是Python而不是C++。别误会,我并不是说额外的功能和能力是坏事,肯定有人会使用这些功能,但说你可以在不断重新学习C++的情况下保持更新,那就是疯了。这门语言似乎已经分裂成越来越多的专门的子组件。 - undefined
显示剩余2条评论

6
根据http://en.cppreference.com/w/cpp/language/member_functions,在成员函数声明后的&lvalue ref-qualifier
换句话说,它要求this是一个l-value(隐式对象参数具有cv限定符X的lvalue引用类型)。还有&&,它要求this是一个r-value。
从文档中复制(const-, volatile-, and ref-qualified member functions):
#include <iostream>
struct S {
  void f() & { std::cout << "lvalue\n"; }
  void f() &&{ std::cout << "rvalue\n"; }
};

int main(){
  S s;
  s.f();            // prints "lvalue"
  std::move(s).f(); // prints "rvalue"
  S().f();          // prints "rvalue"
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接