What are the use cases for having a function return by const value for non-builtin type?

C++Const Correctness

C++ Problem Overview


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?

C++ Solutions


Solution 1 - C++

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.

Solution 2 - C++

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.

Solution 3 - C++

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
}

Solution 4 - C++

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGraemeView Question on Stackoverflow
Solution 1 - C++PuppyView Answer on Stackoverflow
Solution 2 - C++John CalsbeekView Answer on Stackoverflow
Solution 3 - C++Peter AlexanderView Answer on Stackoverflow
Solution 4 - C++yeputonsView Answer on Stackoverflow