类成员函数标记为 && 的用例是什么?

5

我不知道哪个C++标准引入了这个特性,但我想不出任何使用案例。

带有&&修饰符的成员函数只有在对象为右值时才会被重载解析器考虑。

struct Foo 
{
    auto func() && {...}
};

auto a = Foo{};
a.func();            // Does not compile, 'a' is not an rvalue
std::move(a).func(); // Compiles
Foo{}.func();        // Compiles

能否有人解释一下这个用例是什么?
为什么我们希望某些例程仅对rvalue执行?


对于rvalue,它可能会以不同的方式执行,例如放弃资源而不是创建副本。 - BoP
2
有许多用例-不仅仅是“用例”-但通常情况下,只针对rvalues考虑的成员函数不需要保留对象的状态。 - Drew Dormann
2个回答

7

中添加了Ref-qualification。通常,值类别的传播对于泛型编程非常有用!

从语义上讲,函数上的ref-qualifications有助于传达意图;作用于左值引用或右值引用,这类似于函数的constvolatile限定符。 这些也与结构体成员的行为相平行,它们传播修饰符和类别。

实践中一个很好的例子是std::optional,它提供了std::optional::value()函数,该函数将值类别传播到在提取时的引用:

auto x = std::move(opt).value(); // retrieves a T&&

这类似于使用 struct 进行成员访问,其中值类别在访问时传播:
struct Data {
    std::string value;
};

auto data = Data{};

auto string = std::move(data).value; // expression yields a std::string&&

就一般组成而言,这大大简化了输入可能是lvalue或rvalue的情况。例如,考虑使用转发引用的情况:

// Gets the internal value from 'optional'
template <typename Optional>
auto call(Optional&& opt) {
    // will be lvalue or rvalue depending on what 'opt' resolves as
    return std::forward<Optional>(opt).value(); 
}

如果没有ref-qualification,实现上述代码的唯一方法是创建两个静态分支——使用if constexpr或标签分发,或其他方式。类似于:

template <typename Optional>
auto call(Optional&& opt) {
    if constexpr (std::is_lvalue_reference_v<Optional>) {
        return opt.value(); 
    } else {
        return std::move(opt.value()); 
    }
}

在技术层面上,函数的rvalue限定符提供了优化代码移动构造和以语义明确的方式避免复制的机会。
就像当您看到一个std::move(x)时,您可以预期x即将过期;如果您期望std::move(x).get_something()会导致x做同样的事情,这并不是不合理的。
如果您将&&重载与const &重载结合起来,则可以在API中表示不可变复制和可变移动。例如,谦卑的“Builder”模式。通常,Builder模式对象保存将在构造时输入对象的数据片段。这需要在构建过程中进行复制,无论是浅复制还是深复制。对于对象,这可能非常昂贵:
class Builder {
private:
  
    // Will be copied during construction
    expensive_data m_expensive_state;
    ...

public:

    auto add_expensive_data(...) -> Builder&;
    auto add_other_data(...) -> Builder&;
    ...

    auto build() && -> ExpensiveObject {
        // Move the expensive-state, which is cheaper.
        return ExpensiveObject{std::move(m_expensive_state), ...}
    }
    auto build() const & -> ExpensiveObject
        // Copies the expensive-state, whcih is costly
        return ExpensiveObject{m_expensive_state, ...}
    }
    ...
};

没有rvalue限定符,你被迫在实现时做出选择:
1. 在非const函数中执行破坏性操作,只是记录安全性(并希望API不会被错误调用),或者 2. 为了安全起见,复制所有内容
使用rvalue限定符后,它成为调用者的可选功能,并且从编写的代码中清楚地表明意图--无需文档。
// Uses the data from 'builder'. May be costly and involves copies
auto inefficient = builder.build();

// Consumes the data from 'builder', but makes 'efficient's construction
// more efficient.
auto efficient = std::move(builder).build();

作为额外的好处,静态分析通常可以检测到使用后移动的情况,因此比起简单的说明文档,可以更好地捕捉到在 std::move 后意外使用 builder 的情况。

4
struct demo {
    int& get_mem() { return mem; }
private:
    int mem;
};

demo get_demo();

int& ref = get_demo().get_mem();
std::cout << ref; // bug

一个左值引用限定符可以避免这样的错误:
int& get_mem() & { return mem; }
int& ref = get_demo().get_mem(); // safely ill-formed

但这也会阻止之前正确的代码运行,例如:

std::cout << get_demo().get_mem(); // used to be OK; now ill-formed

提供两个不同的修饰符重载函数是一种解决方案:
int& get_mem() &  { return mem; }
int  get_mem() && { return mem; }

它通常与封装其他类型的类型一起使用,并具有完整的重载集,模拟所有constess和value类别:

const int&  get_mem() const& ;
      int&  get_mem()      & ;
const int&& get_mem() const&&;
      int&& get_mem()      &&;

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