0

I am trying to understand the following case, which behaves differently from what I expect.

Given the following code example:

class ConstTestClass
{
    std::deque<unsigned long long> deque;
public:
    const std::deque<unsigned long long int> &getDeque() const{return deque;}
};

int main()
{
    ConstTestClass constTestClass;
    auto constDeque = constTestClass.getDeque();
    constDeque.emplace_back(1ULL);
}

I would expect the call constDeque.emplace_back(1ULL); to fail, since I am expecting constDeque to be a reference to a const std::deque<>, but it actually compiles without any error on GCC 5.4.0.

If I change the line above to const auto constDeque = constTestClass.getDeque(); it fails to compile as expected, since constDeque now is of the expected type.

I am trying to understand why auto can remove the const-ness from the return type and where the error in my understanding lies.

EDIT: This is not answered as a duplicate. I am still looking for a way to make sure the caller always gets a const reference (or pointer) to the internal object, so the caller will see state changes to the itnernal object without being able to modify it.

Jan Henke
  • 875
  • 1
  • 15
  • 28
  • Look up the rules of `auto` type deduction. This creates a copy by value. You need to use `auto const &` or `decltype(auto)`. – underscore_d Jul 19 '16 at 11:53
  • You create a copy here. – Jarod42 Jul 19 '16 at 11:54
  • But why does `auto` remove the cv-qualifier like that? That is the question. In the end I am thinking about design pattern and I do not want the caller to be able to modify the returned object. – Jan Henke Jul 19 '16 at 11:55
  • @JanHenke Because that's what the Standard says it does if not otherwise qualified. – underscore_d Jul 19 '16 at 11:55
  • So you can remove any cv-qualifier from returned library objects that way? That does not seem to make sense. – Jan Henke Jul 19 '16 at 11:56
  • @JanHenke It's not "removing any cv-qualifier" from anything. You're taking a copy of the "library object", with which you can do as you like, because you haven't `const`-qualified it. If you want a `const` reference to it, declare a `const` reference to it. _Then_ you'll find you can't modify it. – underscore_d Jul 19 '16 at 11:56
  • 3
    This is the Standard for C++98 and C++0x. See here http://stackoverflow.com/questions/7138588/c11-auto-what-if-it-gets-a-constant-reference – toro Jul 19 '16 at 11:57
  • But the return type of the getter should impose the cv-qualifier on the returned object. Otherwise, let me ask you, how do I return a const reference to the internal object that cannot be modified by the caller? – Jan Henke Jul 19 '16 at 11:58
  • 1
    @underscore_d I believe the OP is having a problem understanding why `auto` does not deduce to be `const std::deque &` since that is the return type of the function. – NathanOliver Jul 19 '16 at 11:58
  • And as I've explained, the way to understand why `auto` does what it does is to read the Standard. I'm not making a judgement call on whether its behaviour intuitive or advisable: it just _is_. So again, @JanHenke: You **are** returning a `const` reference. But you are not _assigning_ it properly, because `auto` doesn't work how you think it works. So you create a copy. The copy is not `const`, not is it a reference. So if you want a `const` reference, declare it literally, or use `auto const &`. That's just how it works. – underscore_d Jul 19 '16 at 11:59
  • @underscore_d `auto &` should be sufficient, no need for the `const`. – Werner Henze Jul 19 '16 at 12:01
  • Okay, we now agree that `auto` does not retain the cv-qualifier, but how do I make sure then that a caller will always get a `const std::deque &` and not a less qualified object? (at least without explicit use of `const_cast`) – Jan Henke Jul 19 '16 at 12:02
  • @JanHenke The caller will get what you want them to get if they properly accept what you return. If you're asking for a way to prevent them taking a copy, then I think that's an orthogonal question from the use of `auto`, as it becomes dependent on the interface of the returned type. – underscore_d Jul 19 '16 at 12:02
  • @WernerHenze [please ignore my previous totally irrelevant counterexample...] You appear to be correct! I didn't realise the `const` would propagate in that way and thought the attempt to bind a reference not declared `const` would simply fail at compile-time. I guess this is because I tend to always declare a `const` reference to a non-`const` object, rather than the converse. It seems like a good habit to have anyway, signalling intent/awareness even where not required. But thanks for the interesting correction! – underscore_d Jul 19 '16 at 12:08
  • 1
    @JanHenke About your edit. There is nothing you can do. If you return a `const &` then that is all you need to do. You cannot force the caller to take that. They can make a copy, `const_cast` the `const` away and get write access. All you have to do is return a `const &` and let the user do what they want. – NathanOliver Jul 19 '16 at 12:09
  • @NathanOliver They could probably hack together a noncopyable and nonmovable class type to return, but (A) inheriting from an `std::` class is a no-no, so that'd mean a whole interface to reimplement/forward, (B) the constructors/operators thus disabled would easily break other situations, and (C) couldn't RVO kick in and construct the value in-place using the normal constructor? ...which we can't disable without _even_ more hassle. Anyway, I'm sure other threads also discuss potential ways around this. – underscore_d Jul 19 '16 at 12:16
  • @underscore_d I was going with the assumption that we did not change the base type. – NathanOliver Jul 19 '16 at 12:18
  • @NathanOliver Of course! You're perfectly correct that - without veering into a very different question - the answer is 'no'. I'll stop going off-topic. – underscore_d Jul 19 '16 at 12:19
  • Thanks for all the comments (even though I think the down voting of the question was unnecessary). I got confused by the copy, since it is no longer connected to the original internal object and the added member does not get added to the internal object. Also it is good to become aware of the `auto` difficulties. – Jan Henke Jul 19 '16 at 12:21

1 Answers1

2

auto uses the same rules as template argument deduction. In order to clarify it further, imagine that you have a function (e.g., foo):

template<typename T>
void foo(T v) {
  v.emplace_back(1ULL);
}

and then you do the following:

std::deque<unsigned long long> dq;
const std::deque<unsigned long long> &dqr = dq;
foo(dqr);

This code will also pass the compiler Live Demo. This is happening because according to template argument deduction rules T will be deduced to std::deque<unsigned long long> and dqr will be passed by copy to foo.

Now imagine that auto is a template argument (e.g,. T):

T constDeque = constTestClass.getDeque();

Because auto uses the same rules with template argument deduction, as happend in the example in foo, it will deduce T in std::deque<unsigned long long>.

Consequently, you're creating a copy of your const member std::deque member variable. For getting the behaviour you're anticipating, you'll have to give a little help to your auto deduction and write:

auto &constDeque = constTestClass.getDeque();

Or if you want to clarify even further to readers of your code that the resulting auto deduction is a const qualified variable you could add explicitly the const qualifier as:

auto const &constDeque = constTestClass.getDeque();
101010
  • 41,839
  • 11
  • 94
  • 168
  • Interestingly, **Werner Henze** clarified in a comment that if the referred object is `const`, an unqualified `auto &` can be used to declare and will deduce `const &`. I prefer to explicitly include the `const`, but it's worth knowing. e.g.: http://coliru.stacked-crooked.com/a/c54c3cb79232d832 – underscore_d Jul 19 '16 at 12:13
  • 1
    @underscore_d This is due to template argument deduction as well. I agree it worth mentioning it. – 101010 Jul 19 '16 at 12:16