Q:

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:

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?  
Why am I allowed to call l.clear() in the first place, if I have received a const (!) reference?

A:

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.

A:

When you do std::list<int> l = foo.getList();, you create a new list l by copying the returned reference (even if getList() returns a const reference, it's not forbidden to copy it if you want to). So it's later allowed to modify this copy you made.
If you do const std::list<int>& l = foo.getList(); then no copy is made and l remains a const reference to an object you cannot modify.

