Matthias Matthias - 10 months ago 43
C++ Question

How do const references as return values work in C++

I am learning C++ again after a while and have a problem understanding

references as return values. So here is what I've got: a class,
, which holds a
as member:

class Foo
std::list<int> mList;
Foo() { mList.insert(mList.end(), { 1, 2, 3 }); }
size_t size() const { return mList.size(); }

Creating a
object from class
and calling
returns 3, which is fine. Now I want to retrieve this list
, with two requirements:

  • I don't want to allow the caller to modify the original list; and

  • I don't want to create a copy of each list member when retrieving the list.

So after reading a bit on this topic I decided to return a
reference to the list. Thus I added the following method to

const std::list<int>& getList() const { return mList; }

What I expected was that this would return a reference to the list
, so that I can access the original data within this list. But since it's a
reference, I also expected that I would not be able to modify the returned list/reference.

However, playing with this a bit I found out the following:

Foo foo;
cout << foo.size() << endl; // returns 3
std::list<int> l = foo.getList();
cout << foo.size() << endl; // returns 3 again

Now this surprises me and leeds me to two questions:

  1. Since the 2nd
    returns 3 again, the call to
    obviously does not modify the original
    object. But why is that the case if I returned a reference? Is there a copy happening during the return?

  2. Why am I allowed to call
    in the first place, if I have received a
    (!) reference?

Answer Source

getList returns a const& to the list.

One thing you can do, if you choose, with a const& to a list is copy it.

std::list<int> l = foo.getList();

here you choose to copy it. See the std::list<int> l? That isn't a reference. That is an object.

By assigning the const& to it, you said "please copy the contents of the list into my local variable".

This local variable can be cleared, edited, modified, etc.

If you want something that cannot be easily treated like a value, you can write a view type.

template<class It>
struct range_view_t {
  It b; It e;
  It begin() const { return b; }
  It end() const { return e; }
template<class C,
  class It = decltype( std::begin( std::declval<C&>() ) )
range_view_t<It> range_view( C& c ) {
  return {std::begin(c), std::end(c)};

Now a range_view( some_list ) returns an iterable range over the list.

template<class X>
X const& as_const( X& x ) { return x; }

lets you ensure the range is const:

auto r = range_view( as_const( list ) );

gives you a read-only range of iterators into the list.

template<class C>
using range_over = decltype( range_view( std::declval<C&>() ) );
template<class C>
using const_range_over = range_over<const C>;

class Foo
  std::list<int> mList;
  Foo() { mList.insert(mList.end(), { 1, 2, 3 }); }
  size_t size() const { return mList.size(); }
  get_list() const {
    return range_view( mList );

now get_list returns a range view at the mList that can be used in a for(:) loop. Storing a copy of the return value of get_list won't copy anything, as you are just copying a view of something.

Almost always auto is useful here, because the type of the range view is uninteresting to the user.

auto l = foo.get_list();

l has no .clear() method. You can do this:

for( auto&& x : l )
  std::cout << x << "\n";


Finally, note that std::list is almost always the wrong solution to any problem you can describe.