这里的const修饰符是不必要的吗?

45

"Effective C++" 的第3条规则是“尽可能使用const”,并且它给出了一个示例:

const Rational operator*(const Rational& lhs, 
                            const Rational& rhs);

为了防止客户能够像这样犯下暴行:

Rational a, b, c;
...
(a * b) = c;   // invoke operator= on the result of a*b!

但是函数的非引用返回值不已经是rvalue了吗?那为什么还要这样做呢?


6
好的,没有const修饰符,这个赋值语句将编译通过…… - Oliver Charlesworth
由于 a * b 返回一个 const Rational,所以你不能在其上执行 = c 操作。但是我不同意从运算符中返回 const 值。 - rwols
@OliCharlesworth 为什么,非引用返回值肯定是一个rvalue,不能用作=表达式中的左操作数。 - Joey.Z
4
您可以在右值上调用成员函数。 - Koushik Shetty
1
我是在过度解读还是你选择标题语法作为一种修辞手法来强调问题? - Christian Rau
显示剩余2条评论
5个回答

52
对于类类型而言,a=b只是a.operator=(b)的简写(内置类型则不然),其中operator=是一个成员函数。这些成员函数可以在由Rational::operator*创建的(a*b)等右值上调用。为了强制实现与内置右值相似的语义(“像int一样做”),某些作者(包括Meyers)在C++98中建议对具有此类运算符的类进行const-rvalue返回。
但是,在C++11中,通过const-rvalue返回是一个坏主意,因为const rvalues不能绑定到T&&。
在他的笔记中,Scott Meyers在An overview of the new C++ (C++11)中提供了他旧书中确切的例子,并得出结论:现在认为添加const返回值是不好的设计。 推荐的签名现在是:
Rational operator*(const Rational& lhs, const Rational& rhs);

更新:正如在评论中所暗示的@JohannesSchaub-litb,C++11 中您还可以在赋值运算符上使用引用限定符,以便它只接受左值作为其左参数(即 *this 指针,这也是此功能被称为“*this 的右值引用”的原因)。您需要 g++ >= 4.8.1(刚刚发布)或 Clang >= 2.9 才能使用它。


4
因为移动语义要求一个非 const 对象(否则你怎么能 移动 ——也就是修改 ——这个对象呢?)。 - syam
2
@Barmaley.exe:当然,mutable 可以工作,但这会破坏常量正确性:你必须使可移动类的每个成员几乎都是 mutable,那么 const 就不再有任何意义了。 - syam
1
@zoujyjs 用户自定义的赋值运算符如何行为取决于它们的实现。关键是,一般来说,它们应该尽可能地与内建类型的行为相似。但是因为对于类类型,它们也可以在 rvalue 上调用,因此曾经建议将 rvalue 设为 const 以抑制这种赋值。 - TemplateRex
3
@Barmaley.exe说:我完全不同意Andy(你链接的他的答案)。你可以扭曲语言以完成各种任务,但是扭曲语义就是纯粹的邪恶。移动表示修改两个对象(被移动的和移动到的)的可见状态,const表示你不能修改可见状态,这在语义上是不兼容的。仅仅因为你可以自己给自己惹麻烦并不意味着你应该这样做。 - syam
18
禁止此赋值操作的适当方式是 Rational &operator=(const Rational& other) &; - Johannes Schaub - litb
显示剩余8条评论

8

返回值上的const修饰符并不必要,而且可能会阻碍移动语义。在C++11中防止将赋值运算符用于右值的首选方法是使用ref-qualifiers

struct Rational
{
  Rational & operator=( Rational other ) &; // can only be called on lvalues
};

Rational operator*( Rational const & lhs, Rational const & rhs );

Rational a, b, c;

(a * b) = c; // error

5
也许这会让我失去声誉分,但我不同意。不要修改重载运算符的预期返回类型,因为这会让你的类的用户感到烦恼。即使用原本的返回类型。
Rational operator*(const Rational& lhs, const Rational& rhs);

(当然,将参数声明为const是一种好的实践,如果使用常量引用作为参数更佳,因为这样可以避免编译器进行深拷贝。但是在返回值为常量引用时要小心,否则会产生悬垂引用,这是灾难性的。但请注意,在某些情况下,使用引用比按值传递更慢。我认为,在许多平台上,doubleint属于这类情况。)

operator* 的引用返回类型会有些奇怪... - Oliver Charlesworth
也许您可以解释一下为什么返回值最好是非const的?否则,这个答案就不值得太多了。这会如何“惹恼您类的用户”? - interjay
你可能想写成 a = (b * c).some_method_that_is_faster_on_non_const_self()。但我承认我从未遇到过这种情况。 - Fred Foo
Oli:我现在已经编辑了我的答案,并明确表示我不是指返回引用! - Bathsheba
问题是关于具有const值返回类型,而不是const引用返回类型。因此,这个答案似乎不相关。 - interjay
什么是华夫饼??问题涉及返回类型,正如最后一段所示。无论如何,你的“答案”只是一个没有任何理由的个人意见,只是模糊地提到“令人烦恼的用户”,而没有解释为什么。 - interjay

3

由于您可能打算写 (a * b) == c,因此需要进行翻译。

if ((a * b) = (c + d)) // executes if c+d is true

但是你想要

if ((a * b) == (c + d)) // executes if they're equal

9
这是编译器警告应该诊断的问题,禁用移动语义以避免此问题是一个不好的主意。 - Jonathan Wakely

1

我猜你根据问题想要做的是声明相应的operator=为private,以便它不再可访问。

因此,您希望重载与(a*b) = c匹配的签名。 我同意左侧是一个表达式,因此rvalue将是更好的匹配。 但是您忽略了这是函数的返回值,如果您重载函数以返回rvalue,则编译器会抱怨无效的重载,因为重载规则不考虑返回值。

此处所述,赋值运算符的运算符重载始终是类内定义的。如果有一个非成员签名,例如void operator=(foo assignee, const foo& assigner);,则重载分辨率可以将第一部分视为rvalue(然后您可以删除它或将其声明为private)。

所以你可以从两个世界中选择:

  • 要接受用户可能会写出像(a*b) = c这样的愚蠢代码,虽然不是错误,但是会将值存储在无法访问的临时变量中。
  • 使用签名const foo operator*(const foo& lhs, const foo& rhs)来防止(a*b) = c的情况,但会牺牲移动语义。

代码

#include <utility>

class foo {
    public:
    foo() = default;
    foo(const foo& f) = default;
    foo operator*(const foo& rhs) const {
        foo tmp;
        return std::move(tmp);
    }
    foo operator=(const foo& op) const {
        return op;
    }
    private:
    // doesn't compile because overloading doesn't consider return values.
    // conflicts with foo operator=(const foo& op) const;
    foo && operator=(const foo& op) const; 
};


int main ( int argc, char **argv ) {
    foo t2,t1;
    foo t3 = t2*t1;
    foo t4;
    (t2 * t1) = t4;
    return 0;
}
 

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