20

Why does C++ allow the following code to compile?

std::unordered_map<std::string, int> m;
// ...
for (const std::pair<std::string, int>& p: m)
{
    // ...
}

According to Scott Meyers' Effective Modern C++ (p. 40-41):

[...] the key part of a std::unordered_map is const, so the type of std::pair in the hash table (which is what a std::unordered_map is) isn’t std::pair<std::string, int>, it's std::pair <const std::string, int>. But that's not the type declared for the variable p in the loop above. As a result, compilers will strive to find a way to convert std::pair<const std::string, int> objects (i.e., what’s in the hash table) to std::pair<std::string, int> objects (the declared type for p). They’ll succeed by creating a temporary object of the type that p wants to bind to by copying each object in m, then binding the reference p to that temporary object. At the end of each loop iteration, the temporary object will be destroyed. If you wrote this loop, you'd likely be surprised by this behavior, because you'd almost certainly intend to simply bind the reference p to each element in m.

What is the benefit of allowing this implicit conversion? Is there some common use case where the developer would expect / prefer this implicit conversion (rather than getting a compiler error)?

max
  • 49,282
  • 56
  • 208
  • 355
  • 1
    Note that if you used `const auto &` instead of trying to figure out the correct type yourself, you would avoid the copy and arguably get cleaner code. It would also keep things more generic. For instance what if later you needed to change `int` to `long`. Using `auto` would reduce the amount of code churn. – Chris Beck Aug 28 '17 at 18:31
  • @ChrisBeck Yup; in fact, this is precisely what Scott Meyers says in the next paragraph. Still, I wanted to understand the reasons the explicit type specification is so error-prone. – max Aug 30 '17 at 05:52

1 Answers1

22

A standards compliant compiler would "see" the for loop as follows:

auto&& __range = m; 
for (auto __begin = std::begin(m), __end = std::end(m); __begin != __end; ++__begin) { 
    const std::pair<std::string, int>& p = *__begin;
    //loop_statement 
}

Which basically boils down your question to why the following code is allowed:

std::pair<std::string, int> p = std::pair<const std::string, int>{};

Note that I dropped the const& part of p, because it isn't relevant. The conversion is the same, the only difference is that the temporary is bound to a reference instead of being copied.

If you're wondering why OP's snippet doesn't work with a non-const reference, the conversion is the reason why. The result of the conversion is a temporary object, and because any change to the temporary will be useless (its lifetime isn't extended and so it is destroyed right after), so the language disallows it.

This is allowed because std::pair has a constructor that enables this conversion.

template< class U1, class U2 >
pair( const pair<U1, U2>& p );

In your case, U1 is deduced as const std::string and U2 as int. It doesn't actually matter what cv qualifiers U1 and U2 have, because p's elements get copied.

The benefits are the same as to why this is allowed:

const int zero{};
int zero2 = zero;

For example, consider the following non-realistic example:

struct {
    std::pair<int, int> pos;
} player;

std::pair<const int, const int> treasure{1, 2}; // position of treasure
player.pos = treasure; // ok

Now what if, as you say, this conversion were for some reason not allowed. What would the programmer have to do?

player.pos.first = treasure.first;
player.pos.second = treasure.second;

If this would also be disallowed, then the case with the zeroes above would also not be allowed, which doesn't really make sense, because you are copying zero, so it shouldn't matter if you can modify it or not, because that is a totally different operation.

If this is allowed, then why would player.pos = treasure; be disallowed, because the only thing that it does is copying? Like above, it shouldn't matter whether you can change the elements of treasure, because you are only copying them.

This is also why you should use auto&& or const auto& for ranged loops (and maybe even in general?) because it can avoid a copy if you're not careful.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • 7
    This misses the essential point! Have a look at your (correct) translation of the loop! It says: const std::pair& p (REFERENCE!) the question is not why std::pair p = std::pair{}; is allowed, which is trivially allowed since it makes a copy. Of course copying const to non const is possible! The question is why std::pair& p = std::pair{}; (REFERENCE) is allowed. The answer is: it is not allowed. What am I getting wrong? – DrSvanHay Aug 28 '17 at 00:13
  • 1
    @DrSvanHay Well, it is allowed for the exact same reason: The types don't match, so a conversion sequence is searched. I thought the question was why `const pair& = pair2{};` is allowed (note the `const`)? – Rakete1111 Aug 28 '17 at 00:22
  • Yes, but that is not written in your explanation. Is it? there is neither const nor & in that sentence – DrSvanHay Aug 28 '17 at 00:25
  • @Rakete1111 I wonder why the copy constructor `template< class U1, class U2 > pair( const pair& p )` is not made `explicit`. This would still allow copy initialization (and assignment, if desired, through `operator=()`), while disallowing the implicit conversion which (IMO) is likely to cause errors. – max Aug 28 '17 at 00:30
  • @DrSvanHay Is it better now? I'm not sure I worded that correctly (read: understandable) :) – Rakete1111 Aug 28 '17 at 00:31
  • @max Well, not always. See the treasure example. – Rakete1111 Aug 28 '17 at 00:33
  • @Rakete1111 Couldn't `player.pos = treasure;` be allowed by the assignment operator even if the copy constructor is `explicit`? – max Aug 28 '17 at 00:36
  • @max Yes, sure. But then doing `pair p = pair2{};` will fail (maybe surprisingly?). – Rakete1111 Aug 28 '17 at 01:30
  • If someone could point out what is wrong with this answer I would be very grateful. Thanks! – Rakete1111 Aug 28 '17 at 04:25
  • @Rakete1111 Please help me! I do not claim that you got it wrong any more, but I need clarification: You write "Note that I dropped the const& part of p, because it isn't relevant. The conversion is the same, the only difference is that the temporary is bound to a reference instead of being copied." But if that is true, why do we need the const in the const& part of p? Without const it doesnt work. But it should work when the reference simply points to a de-const constructed temporary? – DrSvanHay Aug 28 '17 at 07:55
  • Ok, got it. I´ve two suggestions to make that clearer for unaware readers (like me): You could write template< class T1, class T2 > template< class U1, class U2 > pair::pair( const pair& p ); for the constructor, and you could state that the const is necessary because only a const reference prolongs the lifetime of the temporary. – DrSvanHay Aug 28 '17 at 09:58
  • And one more suggestion. You could write in bold typeface that this stuff is not a compiler workaround or something, but plain use of standard language features. Your post says so, but I think clearly explicitly stating so would be an improvement. (You got an upvote from me) – DrSvanHay Aug 28 '17 at 10:00
  • @DrSvanHay Thanks for helping me! I added a paragraph for that, hope it's better now :) Also, which part is (not) a compiler workaround? – Rakete1111 Aug 28 '17 at 15:29
  • W@Rakete1111 the whole thing SM describes. Even sm Writer that the compiler strives to find a way and a commenter named this thing a workaround. But at the end its just plain vanilla c++ nothing magic or special ccompiler hacks. I find this noteworthy since its considered a hack so offen – DrSvanHay Aug 28 '17 at 15:39
  • I am missing one important point in this answer. Is it a const reference to a temporary allowed ? Yes it is. As you see here: https://stackoverflow.com/questions/2784262/does-a-const-reference-prolong-the-life-of-a-temporary or here: https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/ – Tunichtgut Aug 30 '17 at 12:42