Should I Still Return Const Objects in C++11

Should I still return const objects in C++11?

Returning const objects is a workaround that might cause other problems. Since C++11, there is a better solution for the assignment issue: Reference Qualifiers for member functions. I try to explain it with some code:

int foo(); // function declaration
foo() = 42; // ERROR

The assignment in the second line results in a compile-time error for the builtin type int in both C and C++. Same for other builtin types. That's because the assignment operator for builtin types requires a non-const lvalue-reference on the left hand side. To put it in code, the assignment operator might look as follows (invalid code):

int& operator=(int& lhs, const int& rhs);

It was always possible in C++ to restrict parameters to lvalue references. However, that wasn't possible until C++11 for the implicit first parameter of member functions (*this).

That changed with C++11: Similar to const qualifiers for member functions, there are now reference qualifiers for member functions. The following code shows the usage on the copy and move operators (note the & after the parameter list):

struct Int
{
Int(const Int& rhs) = default;
Int(Int&& rhs) noexcept = default;
~Int() noexcept = default;
auto operator=(const Int& rhs) & -> Int& = default;
auto operator=(Int&& rhs) & noexcept -> Int& = default;
};

With this class declaration, the assignment expression in the following code fragment is invalid, whereas assigning to a local variable works - as it was in the first example.

Int bar();
Int baz();
bar() = baz(); // ERROR: no viable overloaded '='

So there is no need to return const objects. You can restrict the assigment operators to lvalue references, so that everything else still works as expected - in particular move operations.

See also:

  • What is "rvalue reference for *this"?

Should I return const objects?

Top level cv-qualifiers on return types of non class type are ignored.
Which means that even if you write:

int const foo();

The return type is int. If the return type is a reference, of course,
the const is no longer top level, and the distinction between:

int& operator[]( int index );

and

int const& operator[]( int index ) const;

is significant. (Note too that in function declarations, like the above,
any top level cv-qualifiers are also ignored.)

The distinction is also relevant for return values of class type: if you
return T const, then the caller cannot call non-const functions on the
returned value, e.g.:

class Test
{
public:
void f();
void g() const;
};

Test ff();
Test const gg();

ff().f(); // legal
ff().g(); // legal
gg().f(); // **illegal**
gg().g(); // legal

Purpose of returning by const value?

In the hypothetical situation where you could perform a potentially expensive non-const operation on an object, returning by const-value prevents you from accidentally calling this operation on a temporary. Imagine that + returned a non-const value, and you could write:

(a + b).expensive();

In the age of C++11, however, it is strongly advised to return values as non-const so that you can take full advantage of rvalue references, which only make sense on non-constant rvalues.

In summary, there is a rationale for this practice, but it is essentially obsolete.

When is it a good idea to return a const reference in C++?

The decision of when to return by-reference vs. by-value is not just a matter of performance, it's a matter of code semantics (although performance usually matters a lot when coding in C++).

Some notable examples of returning by-reference are:

  • a getter is often expected to return a const-reference to the actual member, unless that member is cheap to copy
  • to allow for method or operator chaining, returning a reference to the current object (*this)

The question of when to return by-reference actually boils down to a broader question of how to manage an object's lifetime safely. C++ Core Guidelines on object's lifetimes is a good resource to adhere to.

If the object being referred-to outlives the function invocation, then it's generally safe to return it by-reference.

So:

  • this, class members and objects with static storage duration: safe to return it by-reference
  • locals and function's input arguments: not safe to return by-reference

Regarding input arguments - it applies even to const references, since they can refer to temporaries. For example:

std::string const& badFunc(std::string const& arg) {
return arg; // not a good idea
}

std::string const& x = badFunc("abc");
// now x contains a dangling reference

Will returning a const object from a function prevent move construction from outside?

In your case, returnedStr will be move-constructed from the return value of GetString(), but that return value will be copy-constructed from str(1). If str wasn't const, the return value would be move-constructed from it.

Note that in both cases, return value optimisation is still applicable, so the compiler can still construct the return value (or even str itself) directly in the space of returnedStr, skipping one or both copy/move constructions. This is granted by C++11 12.8/31:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases,
the implementation treats the source and target of the omitted copy/move operation as simply two different
ways of referring to the same object, and the destruction of that object occurs at the later of the times
when the two objects would have been destroyed without the optimization. This elision of copy/move
operations, called copy elision, is permitted in the following circumstances (which may be combined to
eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a
    non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified
    type as the function return type, the copy/move operation can be omitted by constructing
    the automatic object directly into the function’s return value

  • ...

  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved
    to a class object with the same cv-unqualified type, the copy/move operation can be omitted by
    constructing the temporary object directly into the target of the omitted copy/move

The first bullet point covers the elision of the return value construction, the other one covers moving the return value into returnedStr. Notice the requirement on "the same cv-unqualified" type, which means that this works regardless of cv qualifiers.


(1) Note that if we were talking about a class X other than std::string, one which provided a move constructor taking a const X&&, then indeed the return value would be move constructed using this constructor (whatever semantics it might have).

Which are the implications of return a value as constant, reference and constant reference in C++?

Here's the lowdown on all your cases:

• Return by reference: The function call can be used as the left hand side of an assignment. e.g. using operator overloading, if you have operator[] overloaded, you can say something like

a[i] = 7;

(when returning by reference you need to ensure that the object you return is available after the return: you should not return a reference to a local or a temporary)

• Return as constant value: Prevents the function from being used on the left side of an assignment expression. Consider the overloaded operator+. One could write something like:

a + b = c; // This isn't right

Having the return type of operator+ as "const SomeType" allows the return by value and at the same time prevents the expression from being used on the left side of an assignment.

Return as constant value also allows one to prevent typos like these:

if (someFunction() = 2)

when you meant

if (someFunction() == 2)

If someFunction() is declared as

const int someFunction()

then the if() typo above would be caught by the compiler.

• Return as constant reference: This function call cannot appear on the left hand side of an assignment, and you want to avoid making a copy (returning by value). E.g. let's say we have a class Student and we'd like to provide an accessor id() to get the ID of the student:

class Student
{
std::string id_;

public:

const std::string& id() const;
};

const std::string& Student::id()
{
return id_;
}

Consider the id() accessor. This should be declared const to guarantee that the id() member function will not modify the state of the object. Now, consider the return type. If the return type were string& then one could write something like:

Student s;
s.id() = "newId";

which isn't what we want.

We could have returned by value, but in this case returning by reference is more efficient. Making the return type a const string& additionally prevents the id from being modified.

Keep constant reference to return value of function in C++

Your code is illegal; non-const lvalue references may not bind to rvalues. There's not really a good reason behind this, it's just a language rule that was introduced very early on in C++'s history.

MSVC used to (maybe still does) allow this binding, I can't comment on how MSVC implements it.

You can bind to other reference types though:

std::string const &a = foo();   // (1)
std::string&& b = foo(); // (2)

In case (2), b binds directly to the return value object, which has its lifetime extended to match b's lifetime. Note: no "move" operation occurs here, it is just binding a reference.

In case (1), conceptually, a temporary of type const std::string is initialized from the return value, and that temporary has its lifetime extended to match a's lifetime. In practice this copy will be elided. your code will behave as if the reference bound directly to the return value.


Generally speaking, you should use value semantics. std::string c = foo(); is the safest option. Because of copy elision, it is not any less efficient than the reference options.

The main danger with the reference option is that if the function were changed to return a reference, then a or b may become a dangling reference.

returning constant object and assigning it to non-constant object

So, is it okay that constness of a temporary object is simply ignored by RVO?

Yes. [class.copy]/p31 (quoting N4527, which incorporates some DRs that clarifies the intent; emphasis mine):

This elision of copy/move operations, called copy elision, is
permitted in the following circumstances (which may be combined to
eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a nonvolatile automatic object (other
    than a function parameter or a variable introduced by the
    exception-declaration of a handler (15.3)) with the same type (ignoring cv-qualification) as the function return type, the copy/move
    operation can be omitted by constructing the automatic object directly
    into the function’s return value
  • [...]
  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same type
    (ignoring cv-qualification), the copy/move operation can be omitted by
    constructing the temporary object directly into the target of the
    omitted copy/move
  • [...]

The third bullet is the one applicable here; note that a similar rule applies to NRVO (first bullet) as well.



Related Topics



Leave a reply



Submit