7

As title.

This compile error occurs when using the std::get<T>(pair), where the first member of the pair is a const, coming off an iterator for std::map or std::unordered_map.

To test the compile error, comment out the "notstd" overload of get.

I have researched this question on Stack Overflow with the three most relevant questions listed below.

The existing answers lead me to believe that it should be a defect report, that the respective std::get overload should have been added to the standard library, and that the automatic lifetime extension applied to temporary constant reference should be extended to cover such cases.

I have also researched whether it has to do with specialization of layouts (question 14272141, linked below). However, my code snippet only asks for the const reference to one of the two members; even with specialization of layouts, the const reference to either member should still exist.

I understand that casting between const std::pair<T, U>& and const std::pair<const T, U>& would not be safe, based on the existing answers.

namespace notstd
{
    template <class T, class U>
    const T& get(const std::pair<const T, U>& tu)
    {
        return tu.first;
    }
}
int test(int value) 
{
    using namespace notstd;
    using namespace std;
    const std::pair<const int, bool> one(value, false);
    const auto two = get<int>(one);
    const auto three = get<const int>(one);
    return 0;
}

Questions with high relevance:

(Humility notice: even though I claimed that this seems like a defect report, there may be some missing knowledge in my head, so please do let me know. From rioki's answer below, I can see that the current design allows distinguishing the two arguments in a std::pair<const int, int> whereas my proposal would fail.)

My characterization of the present situation:

  • Annoying inconsistency, because the issue arises when two idioms (typed-get, and consistent use of range-based-for from vector<pair> and unordered_map<pair>) are used together, and the explanation for this incompatibility is not exactly palatable to anyone including beginners and experienced programmers.
  • One or more satisfactory workarounds exist, as explained in rioki's answer.
  • Perhaps not so much of a defect report (because existing code may have depended on the ability to distinguish between a const int from an int.), or that it cannot be improved without breaking existing code.
rwong
  • 6,062
  • 1
  • 23
  • 51
  • 2
    Automatic lifetime extension is very unlikely to ever apply across call frames, because that would violate the rule of "You don't pay for what you don't use". – Ben Voigt Jan 29 '18 at 22:01
  • @benv if we could mark up said dependencies in ctors and function signatures, we could get it without paying for it. – Yakk - Adam Nevraumont Jan 29 '18 at 22:56
  • @Yakk: Return-by-value is reflected in the function signature, and elision rules should let the compiler perform lifetime extension instead of copying/moving in most cases. Marking on ctor parameters might allow avoiding the cost when it isn't used, but I'm not convinced. How would `make_unique` or `make_shared` arrange for the lifetime of the accompanying temporaries match the lifetime of the dynamically allocated object? – Ben Voigt Jan 29 '18 at 23:04
  • @ben shared is easy: type erased cleanup. Unique would error. – Yakk - Adam Nevraumont Jan 29 '18 at 23:28
  • @Yakk: The compiler would have to allocate reference-counted dynamic memory for all temporaries that were passed as reference to a function with your "take ownership" marking, just in case the function would pass them through to a constructor. And it would have to be type-safe, because the function could pass them through to construction of a new object, pass that to another thread, and then do some more work. And either the death of the object or completion of the original function might be the trigger for cleanup. At least (I think) circular references should not be an issue. – Ben Voigt Jan 30 '18 at 00:26
  • @Yakk: But what if the object under construction decided it didn't need to store a reference to the temporary passed in, after all? Temporary lifetime would become very difficult to reason about. Manually setting up a smart pointer is far far nicer than the hoops to make cross-frame temporary lifetime extension work. – Ben Voigt Jan 30 '18 at 00:27

2 Answers2

12

Is this a defect in C++ that std::get<T>(const std::pair<const T, U>& ) fail to compile due to const T?

No. I would very much expect this to fail:

std::pair<const int, bool> p(42, true);
std::get<int>(p); // expected failure

std::get<T> means to retrieve the element whose type is T. Not the element whose type approximates T, or decays to T, or any other such. std::get comes to pair by way of tuple, where it is specified as:

Requires: The type T occurs exactly once in Types.... Otherwise, the program is ill-formed.

If we consider pair as a special case of tuple, int does not occur exactly once in {const int, bool}, so the program should be ill-formed.

Put in other words: you are asking for the int in that pair, but there is no int there. There's a const int and there's a bool.

Barry
  • 286,269
  • 29
  • 621
  • 977
0

What are you trying to achieve?

I would question the wisdom of using a const value type in pair in the first place. What do you believe you are achieving with that const?

If you pretend there is no std::pair you would implement it something like this:

struct my_pair
{ 
    const int first;
    const bool second;
}; 

But what does that mean for the type. For starters it is not assignable. Generally the use of pair is in copyable context, since you tend to want to pass two related values around without needing to build separate class. If you use a pair on the stack, you may as well just use two variables.

But the underlying issue here is a bit different. Imagine you have the following pair:

std::pair<const int, int> p;

If you use typed std::get, this works:

auto first  = std::get<const int>(p);
auto second = std::get<int>(p);

Then again, I never really understood why typed get is even a thing. Since this is way clearer

auto first  = std::get<0>(p);
auto second = std::get<1>(p);

Or, in the case of pair, just use the good old:

auto first  = p.first;
auto second = p.second;

So I would consider your question a non issue...

rioki
  • 5,988
  • 5
  • 32
  • 55
  • 6
    This compile error occurs when using `std::get(*iter)`. The pair comes from dereferencing a const-iterator for `std::map` or `std::unordered_map`. The STL map or unordered_map automatically adds const qualifier to the key to prevent programmers from trying to modifying it. In-place modification of the key would have caused map or unordered_map to perform incorrect operations. Thus, the presence of the const qualifier inside the pair definition is understandable. I understand but politely disagree that the alternatives are just as good as typed get. Sometimes explicit type is better. – rwong Jan 29 '18 at 21:33
  • I get a valid point from your answer: if the pair is defined as `std::pair`, then there seems to be a need to distinguish between the first and the second, and therefore it would have been bad to assume equivalence between the two. – rwong Jan 29 '18 at 21:37
  • 4
    By "not copyable" did you really mean "not assignable"? Types with `const` members are perfectly copyable. – Ben Voigt Jan 29 '18 at 22:03
  • @rwong I agree with you, there are uses, but they tend not to be common or assignable. Also I agree with explicit typing, but I find the non positional get functions to be worse. Especially, once you get things like map... – rioki Feb 01 '18 at 22:44