Matthias Matthias - 4 days ago 6
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

const
references as return values. So here is what I've got: a class,
Foo
, which holds a
std::list
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
foo
object from class
Foo
and calling
foo.size()
returns 3, which is fine. Now I want to retrieve this list
mList
, 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
const
reference to the list. Thus I added the following method to
Foo
:

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


What I expected was that this would return a reference to the list
mList
, so that I can access the original data within this list. But since it's a
const
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();
l.clear();
cout << foo.size() << endl; // returns 3 again


Now this surprises me and leeds me to two questions:


  1. Since the 2nd
    cout
    returns 3 again, the call to
    clear()
    obviously does not modify the original
    foo.mList
    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
    l.clear()
    in the first place, if I have received a
    const
    (!) reference?


Answer

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(); }
  const_range_over<std::list<int>>
  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";

however.

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

Comments