What Are the Use Cases for Having a Function Return by Const Value for Non-Builtin Type

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

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.

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.

Why is returning a const from a function not being detected as a const?

y() is a prvalue expression. The type of this expression is not const int, but int.

This is because the type of prvalue non-class non-array expressions have their cv-qualifiers stripped.

In other words, it will work if you use a class type, but not with non-class types.

This is just how the language works. There is no difference between a const int and a int prvalue and similar for other fundamental types. They are just values for which the qualifiers aren't useful.

In contrast the expression x is a lvalue, not a prvalue, and therefore the cv-qualifier is not stripped. There is a difference between referring to an object via const-qualified and non-const-qualified lvalue.

But even then decltype applied directly to an non-parenthesized name doesn't actually consider the type of the expression anyway, but instead considers the type of the declaration of the named entity. This is a special case. decltype((x)) would consider the expression type and yield const int&, adding the lvalue reference because x is a lvalue.

std::invoke_result is also specified to return the decltype of the INVOKE expression, so it will have the same issue.


You can obtain the const-qualified type from the return type of the function type. A typical approach to that is partial specialization based on the function type. Unfortunately doing this properly is quite cumbersome since a lot of specializations must be written out to cover all cases. It also won't work if y is overloaded or a generic lambda/functor.

My guess would be that e.g. boost::callable_traits::return_type is implemented that way and will yield the const-qualified int.

It produces the expected result (see https://godbolt.org/z/7fYn4q9vs):

#include <type_traits>
#include <iostream>
#include <boost/callable_traits/return_type.hpp>

inline const int x = 1;
/// A constant integer variable.

inline const int y() {return x;}
/// A constant integer function returning <x>.

int main()
{
/// <x> successfully passes as being a constant.
std::cout << std::is_same_v <const int, decltype(x)> << " ";

/// And returning it from a function (<y>) now does as well.
std::cout << std::is_same_v <const int, boost::callable_traits::return_type_t<decltype(y)>> << std::endl;
}

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"?

putting 'const' before declaring function in a class

const string get_name()

means that the return value is constant object. It prevents the caller of the function from calling non-const methods on the temporary return value, e.g.

x.get_name() = "bla"
x.get_name()[0] = 'a';

will not compile.

I think one of the main motivations was to prevent accidental assignments e.g. in if statements:

if (x.get_name() = "John Doe"))

It has been a guideline for some time, but C++11 made it obsolete by introducing rvalue references and move semantics.

In your example, ypu should probably return a const reference to name. This will save you the creation of temporary object and keep the benefits of being const. And the method should be declared const because it does not modify the object's state.

const& get_name() const { return name; };

const Class * const Function() What does the second const do?

const SimpleClass * const Myfunction()
{
return sc;
}

decltype(auto) p = Myfunction();
p = nullptr; // error due to the second const.

But the truth is that not many people uses decltype(auto), and your function will be normally called like:

const SimpleClass *p = Myfunction();
p = nullptr; // success, you are not required to specify the second const.

const auto* p = Myfunction();
p = nullptr; // success, again: we are not required to specify the second const.

And...

const SimpleClass * const p = Myfunction();
p = nullptr; // error

const auto* const p = Myfunction();
p = nullptr; // error

Returning a const pointer to a const data member and the 'auto' keyword. A bit confused

As already mentioned, auto ignores top level cv-qualifiers. Read this article to learn the details of how auto and decltype work.

Now, even if auto didn't ignore the const, in your case, temp would still not be const because top level cv-qualifiers on return types are ignored if the type being returned is of non-class type.

g++ even produces the following warning with -Wextra

warning: type qualifiers ignored on function return type [-Wignored-qualifiers]

This can be demonstrated by using C++14's decltype(auto). Unlike auto, decltype(auto) doesn't discard references and top level cv-qualifiers. If you modify your example by adding the following lines the code will still compile, proving that temp is not a const pointer.

decltype(auto) temp = myClass.test();
static_assert(std::is_same<const int*, decltype(temp)>{}, "");

On the other hand, if test() returns an object of class type with a top level cv-qualifier, then auto would still discard const, but decltype(auto) wouldn't.

Live demo

In C++, is a const method returning a pointer to a non-const object considered bad practice?

It depends on whether the non-const pointer is pointing to something inside the const object, or whether it is newly allocated memory that is meant to be variable.

Consider something that returned a copy of a string; it's fine for the returned copy to be non-const, even if the original string is const. The ownership of the copy is being passed from the method to the calling code; ensuring that the copy is appropriately destructed is now the calling code's problem (quite possibly a very minor problem).

However, it would be a very bad idea to return a non-const pointer to something from inside a const object.



Related Topics



Leave a reply



Submit