对于非内置类型,使用const值返回函数有什么用例?

时间:2011-06-09 22:15:14

标签: c++ const-correctness

最近我读过,从函数返回值来限定非内置类型的返回类型const是有意义的,例如:

const Result operation() {
    //..do something..
    return Result(..);
}

我很难理解这个的好处,一旦返回了对象,确定调用者的选择是决定返回的对象是否应该是const?

4 个答案:

答案 0 :(得分:34)

基本上,这里有轻微的语言问题。

std::string func() {
    return "hai";
}

func().push_back('c'); // Perfectly valid, yet non-sensical

返回const rvalues是为了防止这种行为。然而,实际上,它确实弊大于利,因为现在rvalue引用就在这里,你只是要防止移动语义,这很糟糕,并且通过明智地使用rvalue和lvalue可能会阻止上述行为*this重载。另外,无论如何,你必须要做一点白痴。

答案 1 :(得分:12)

偶尔会有用。见这个例子:

class I
{
public:
    I(int i)                   : value(i) {}
    void set(int i)            { value = i; }
    I operator+(const I& rhs)  { return I(value + rhs.value); }
    I& operator=(const I& rhs) { value = rhs.value; return *this; }

private:
    int value;
};

int main()
{
    I a(2), b(3);
    (a + b) = 2; // ???
    return 0;
}

请注意,operator+返回的值通常被视为临时值。但它显然正在被修改。这不是完全想要的。

如果将operator+的返回类型声明为const I,则无法编译。

答案 2 :(得分:9)

按价值返回没有任何好处。这没有意义。

唯一的区别是它阻止人们将它用作左值:

class Foo
{
    void bar();
};

const Foo foo();

int main()
{
    foo().bar(); // Invalid
}

答案 3 :(得分:0)

去年,我在处理双向C ++到JavaScript绑定时发现了另一个令人惊讶的用例。

它需要以下条件的组合:

  • 您有一个可复制且可移动的类 '*': (a, b) => a * b, '/': (a, b) => a / b, SWAP: (a, b) => [b, a], ROT: (a, b, c) => [b, c, a],
  • 您有一个Base派生的不可复制的不可移动类Derived
  • 您确实非常不希望Base中的Base实例也可移动。
  • 但是,您真的想以任何原因进行切片。
  • 所有类实际上都是模板,并且您想使用模板类型推导,因此您不能真正使用Derived或类似的技巧代替公共继承。
Derived::operator const Base&()

我没有为#include <cassert> #include <iostream> #include <string> #include <utility> // Simple class which can be copied and moved. template<typename T> struct Base { std::string data; }; template<typename T> struct Derived : Base<T> { // Complex class which derives from Base<T> so that type deduction works // in function calls below. This class also wants to be non-copyable // and non-movable, so we disable copy and move. Derived() : Base<T>{"Hello World"} {} ~Derived() { // As no move is permitted, `data` should be left untouched, right? assert(this->data == "Hello World"); } Derived(const Derived&) = delete; Derived(Derived&&) = delete; Derived& operator=(const Derived&) = delete; Derived& operator=(Derived&&) = delete; }; // assertion fails when the `const` below is commented, wow! /*const*/ auto create_derived() { return Derived<int>{}; } // Next two functions hold reference to Base<T>/Derived<T>, so there // are definitely no copies or moves when they get `create_derived()` // as a parameter. Temporary materializations only. template<typename T> void good_use_1(const Base<T> &) { std::cout << "good_use_1 runs" << std::endl; } template<typename T> void good_use_2(const Derived<T> &) { std::cout << "good_use_2 runs" << std::endl; } // This function actually takes ownership of its argument. If the argument // was a temporary Derived<T>(), move-slicing happens: Base<T>(Base<T>&&) is invoked, // modifying Derived<T>::data. template<typename T> void oops_use(Base<T>) { std::cout << "bad_use runs" << std::endl; } int main() { good_use_1(create_derived()); good_use_2(create_derived()); oops_use(create_derived()); } 指定类型实参的事实意味着编译器应该能够从参数的类型推论得出,因此要求oops_use<>实际上是{{ 1}}。

调用Base<T>时应该发生隐式转换。为此,将Derived<T>的结果具体化为一个临时oops_use(Base<T>)值,然后通过create_derived() move构造函数将其移到Derived<T>的参数中。因此,现在将物化临时对象移出,并且断言失败。

我们无法删除该move构造函数,因为它将使oops_use不可移动。而且,我们无法真正阻止Base<T>(Base<T>&&)绑定到Base<T>(除非我们显式删除Base<T>&&,这对于所有派生类都应如此)。

因此,此处唯一未经Derived<T>&&修改的解决方案是使Base<T>(Derived<T>&&)返回Base,以便create_derived()的参数构造函数不能从物化临时变量中移出。 / p>

我喜欢这个示例,因为它不仅在有和没有const Derived<T>的情况下都进行编译而没有任何未定义的行为,并且在有和没有oops_use的情况下其行为都不同,并且正确的行为实际上在const下发生