"Reference qualifier correctness"还是应该将非常量方法应用于右值?

"Reference qualifier correctness" or should a non-const method ever apply to rvalues?

本文关键字:方法 常量 应用于 非常 qualifier Reference correctness      更新时间:2023-10-16

现在GCC 4.8.1和Clang 2.9及更高版本支持它们,引用限定符(也称为" *this的右值引用")已经变得更加广泛可用。它们允许类的行为更像内置类型,例如,不允许对右值赋值(否则会导致不必要的将右值强制转换为左值):

class A
{
    // ...
public:
    A& operator=(A const& o) &
    {
        // ...
        return *this;
    }
};

一般来说,调用右值的const成员函数是明智的,因此左值引用限定符将不合适(除非右值限定符可用于优化,例如将成员移出类而不是返回副本)。

另一方面,像预减/自增操作符这样的变异操作符应该是左值限定的,因为它们通常返回对对象的左值引用。因此也有问题:是否有任何理由允许突变/非const方法(包括操作符)在右值引用上调用,除了概念上的const方法,这些方法仅未标记为const,因为常量正确性(包括使用内部缓存时正确应用mutable,现在可能包括确保某些线程安全保证)在代码库中被忽略?

澄清一下,我并不是建议禁止在语言级别上修改左值的方法(至少这可能会破坏遗留代码),但我相信默认(作为一种习惯用法/编码风格)只允许左值修改方法通常会导致更干净、更安全的api。然而,我对那些不这样做会导致更干净、更少令人惊讶的api的例子更感兴趣。

如果r值用于完成某些任务,则对r值进行操作的mutator是有用的,但在此期间它保持某种状态。例如:

struct StringFormatter {
     StringFormatter &addString(string const &) &;
     StringFormatter &&addString(string const &) &&;
     StringFormatter &addNumber(int) &;
     StringFormatter &&addNumber(int) &&;
     string finish() &;
     string finish() &&;
};
int main() {
    string message = StringFormatter()
            .addString("The answer is: ")
            .addNumber(42)
            .finish();
    cout << message << endl;
}

通过允许l值或r值,可以构造一个对象,将其传递给一些变量,并使用表达式的结果来完成某些任务,而不必将其存储在l值中,即使这些变量是成员函数。

还要注意,并非所有的变异操作符都返回对self的引用。用户定义的突变器可以实现他们需要或想要的任何签名。mutator可以消耗对象的状态以返回更有用的东西,并且通过作用于r值,对象被消耗的事实不是问题,因为否则状态将被丢弃。实际上,使用对象状态以产生其他有用内容的成员函数必须这样标记,以便更容易看到l值何时被使用。例如:

MagicBuilder mbuilder("foo", "bar");
// Shouldn't compile (because it silently consumes mbuilder's state):
// MagicThing thing = mbuilder.construct();
// Good (the consumption of mbuilder is explicit):
MagicThing thing = move(mbuilder).construct();

我认为它出现在检索某些值的唯一方法是通过改变另一个值的情况下。例如,迭代器不提供"+1"或"next"方法。因此,假设我正在为stl列表迭代器构造一个包装器(可能是为我自己的列表支持的数据结构创建一个迭代器):

class my_iter{
private:
    std::list::iterator<T> iter;
    void assign_to_next(std::list::iterator<T>&& rhs) {
        iter = std::move(++rhs);
    }
};

这里,assign_to_next方法接受一个迭代器,并将这个迭代器赋值为它之后的下一个位置。不难想象这可能有用的情况,但更重要的是,这个实现没有什么令人惊讶的。没错,我们也可以说iter = std::move(rhs); ++iter;++(iter = std::move(rhs));,但我看不出有任何理由说明为什么这些会更干净或更快。我认为这样的实现对我来说是最自然的。

关于赋值操作符,FWIW hc++同意您的看法:

http://www.codingstandard.com/rule/12-5-7-declare-assignment-operators-with-the-ref-qualifier/


非const方法应该应用于右值吗?

这个问题把我难住了。对我来说,更明智的问题是:

const方法应该只应用于右值吗?

我认为答案是否定的。我无法想象会有重载const右值*this的情况,就像我无法想象重载const右值实参的情况一样。

你重载右值,因为当你知道你可以窃取它们的内部时,你可以更有效地处理它们,但是你不能窃取const对象的内部。

有四种可能的方法重载*this:
struct foo {
    void bar() &;
    void bar() &&;
    void bar() const &;
    void bar() const &&;
};

后两个重载的一致性意味着它们都不能改变*this,所以const &重载允许对*this做的事情和const &&重载允许对*this做的事情之间没有区别。在没有const &&重载的情况下,const &无论如何都将绑定到左值和右值。

考虑到const &&上的重载是无用的,并且只是为了完整性而提供的(证明我错了!),我们只剩下一个ref_qualifier的用例:重载非const右值*this。可以为&&重载定义函数体,也可以为= delete重载定义函数体(如果只提供&重载,则会隐式地进行此操作)。我可以想象在很多情况下,定义&&函数体可能是有用的。

通过重载operator->和一元operator*(如boost::detail::operator_arrow_dispatch)实现指针语义的代理对象可能会发现在其operator*上使用reff限定符很有用:

template <typename T>
struct proxy {
    proxy(T obj) : m_obj(std::move(obj)) {}
    T const* operator->() const { return &m_obj; }
    T operator*() const& { return m_obj; }
    T operator*() && { return std::move(m_obj); }
private:
    T m_obj;
};

如果*this是右值,那么operator*可以通过移动而不是复制返回。

我可以想象函数从实际对象移动到参数。