61

Recently I have read that it makes sense when returning by value from a function to qualify the return type const for non-builtin types, e.g.:

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

I am struggling to understand the benefits of this, once the object has been returned surely it's the callers choice to decide if the returned object should be const?

Graeme
  • 4,514
  • 5
  • 43
  • 71

4 Answers4

52

Basically, there's a slight language problem here.

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

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

Returning const rvalues is an attempt to prevent such behaviour. However, in reality, it does way more harm than good, because now that rvalue references are here, you're just going to prevent move semantics, which sucks, and the above behaviour will probably be prevented by the judicious use of rvalue and lvalue *this overloading. Plus, you'd have to be a bit of a moron to do this anyway.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • Are you sure that marking the rvalue as const prevents move semantics? My version of clang lets a custom move constructor get called with a const rvalue (though it doesn't let a non-const rvalue reference get bound to it, so I'm not entirely sure what's going on). – John Calsbeek Jun 09 '11 at 22:29
  • @John Calsbeek: How can you move from a const rvalue? You can't modify it- because it's const. – Puppy Jun 09 '11 at 22:46
  • @John: But can your const move constructor really perform the move? E.g. suppose your class had a raw pointer that needed to be freed. In the move constructor, you copy the pointer, but you also have to set the copy in the RHS object to NULL so it doesn't get double-deleted. But if it's a const rvalue, you can't modify it... – HighCommander4 Jun 09 '11 at 22:46
  • 5
    It is nonsensical for `void` methods. For methods that return something it can make perfect sense. For example, if `string::push_back` returned a reference to the string itself, you'd be able to do `string s = func().push_back('c');` (for one example). – AnT stands with Russia Jun 09 '11 at 22:48
  • I managed to get my version of clang to, via a move constructor, bind a `const T` to a `T&&`, and mutate it through the rvalue reference. I'm just trying to figure out if that's a bug in clang or not. (I can't think of a "truly const" rvalue, besides one that was casted from a lvalue that was declared as const.) – John Calsbeek Jun 09 '11 at 22:49
  • @John: const object being bound to non-const reference... rvalue or not, that violates const correctness and should not be allowed. My guess is it's a bug – HighCommander4 Jun 09 '11 at 22:54
  • It's how they used to be, but the spec was since changed. – Puppy Jun 10 '11 at 01:09
  • @John: if you activate the warnings on CLang, does not it say that the top level qualifiers of the return type are ignored, ie that it treats `T volatile * const` as `T volatile *` ? – Matthieu M. Jun 10 '11 at 06:34
  • For newer classes, I would recommend making `push_back()` a `&`-qualified method, like `void push_back() &;`. It'll prohibit calling it on temporary values. I think it's better option, because "`push_back` should not be called on a temporary" is really a property of `push_back`, not `func()`. It, however, still allows nonsense like `func() = "foo"`. – yeputons Oct 05 '20 at 21:29
23

It is occasionally useful. See this example:

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;
}

Note that the value returned by operator+ would normally be considered a temporary. But it's clearly being modified. That's not exactly desired.

If you declare the return type of operator+ as const I, this will fail to compile.

John Calsbeek
  • 35,947
  • 7
  • 94
  • 101
11

There is no benefit when returning by value. It doesn't make sense.

The only difference is that it prevents people from using it as an lvalue:

class Foo
{
    void bar();
};

const Foo foo();

int main()
{
    foo().bar(); // Invalid
}
Peter Alexander
  • 53,344
  • 14
  • 119
  • 168
  • 9
    This is actually incorrect. It doesn't prevent people from using it as an lvalue at all. It prevents people from using it as a non-const object. These are two completely different things. – Puppy May 05 '17 at 12:00
  • class Foo { public: void bar() {}; }; Foo foo() { return Foo();} ; – Nusrat Nuriyev Jul 05 '23 at 12:50
5

Last year I've discovered another surprising usecase while working on a two-way C++-to-JavaScript bindings.

It requires a combination of following conditions:

  • You have a copyable and movable class Base.
  • You have a non-copyable non-movable class Derived deriving from Base.
  • You really, really do not want an instance of Base inside Derived to be movable as well.
  • You, however, really want slicing to work for whatever reason.
  • All classes are actually templates and you want to use template type deduction, so you cannot really use Derived::operator const Base&() or similar tricks instead of public inheritance.
#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());
}

The fact that I did not specify the type argument for oops_use<> means that the compiler should be able to deduce it from argument's type, hence the requirement that Base<T> is actually a real base of Derived<T>.

An implicit conversion should happen when calling oops_use(Base<T>). For that, create_derived()'s result is materialized into a temporary Derived<T> value, which is then moved into oops_use's argument by Base<T>(Base<T>&&) move constructor. Hence, the materialized temporary is now moved-from, and the assertion fails.

We cannot delete that move constructor, because it will make Base<T> non-movable. And we cannot really prevent Base<T>&& from binding to Derived<T>&& (unless we explicitly delete Base<T>(Derived<T>&&), which should be done for all derived classes).

So, the only resolution without Base modification here is to make create_derived() return const Derived<T>, so that oops_use's argument's constructor cannot move from the materialized temporary.

I like this example because not only it compiles both with and without const without any undefined behaviour, it behaves differently with and without const, and the correct behavior actually happens with const only.

yeputons
  • 8,478
  • 34
  • 67
  • Of course, that initial set of requirements violates Liskov substitution principle. – yeputons Dec 26 '20 at 08:52
  • 1
    Also, a more robust approach here would be to modify `Base` so that it cannot move from non-movable derived classes by adding a templated SFINAEd copy constructor. – yeputons Dec 26 '20 at 08:53