Date: Thu, 29 Jul 2021 10:26:08 -0500
Thanks, all, for the excellent discussion.
Although I would prefer optional<T&> to be a supported construct (I tend to agree with Barry re. its usefulness), I do not want such discussions to distract from the utility I'm suggesting here. To that end, whether the function returns a bare pointer instead of optional<T&> is secondary to whether there is enough semantic improvement to warrant a new member function at all.
Responding to a few comments:
From Andrey:
> Does this feature extend to multimap/unordered_multimap? If so, the
> returned type would be some sort of a range that could be tested for
> being empty via a contextual conversion to bool. That would also work
> for map/unordered_map.
I don't think this proposal applies well to multimaps. The proposed utility is a non-throwing version of std::map::at in the case of an absent key. Perhaps a better name for it would be std::map::ptr_at. For multimaps, the equal_range member function seems adequate.
From Tony:
> Do you also want to be able to change the value being referenced?
>
> if (auto age = ages.value_for(name)) {
> age++; // it's a new year!
> } else {
> std::cout << name << "not known\n:";
> }
For a non-const ages, yes. To use your example, the point is to make the following replacement:
Current: if (auto it = ages.find(name); it != end(ages)) { ++it->second; }
Proposed: if (auto age = ages.find(name)) { ++*age; }
From Jason:
> The difference in lines here is:
>
> Current:
> if(auto it = ages.find(name); it != ages.end())
>
> Proposed:
> if(auto age = ages.value_for(name)))
>
> So, you're talking about a difference of twelve characters. Not
> exactly what I would call "boilerplate".
You're missing the 'it->second' vs. '*age' access pattern, which is perhaps the primary motivator of this proposal. And number of characters, although important, is subservient to the potential semantic improvement of a proposed utility. For example:
C++17: if (ages.find(name) != cend(ages)) { /* name present */ }
C++20: if (ages.contains(name)) { /* name present */ }
We're not talking about a lot of characters difference here, but providing the 'contains' function was a substantial-enough semantic improvement that the standards committee went for it. The question is whether adding a 'ptr_at' or 'value_for' member function provides similar semantic improvement. I'm not sure yet.
> Also, it normalizes practice that is somewhat dubious. Indeed, we
> added special variable declaration syntax to C++17 specifically to
> *discourage* this kind of thing.
Hmm, the main reason for adding an optional init statement that precedes the if condition was to limit the scope of variables that do not belong outside of if...else code. So although this is a related issue, I'm not sure it is directly germane to the facility being proposed here.
From Zhihao, in response to Jason:
> I wouldn't say the second form
> is discouraged. But it does
> have a drawback: what if you only
> want the false branch?
If you want the false branch of whether an item is present in the map, then you have no need of the (nonexistent) value and you would call 'if (not ages.contains(name))' instead.
From Arthur:
> ...Which goes to illustrate what I think is my point:
> helper methods are super useful but also have way too many knobs to be
> worth standardizing.
That is the correct default response, Arthur. Free functions like you presented should generally be favored. However, when a particular coding pattern is (a) prevalent among worldwide (e.g.) std::map users, and when (b) its access pattern could be improved in terms of readability without degrading performance, it's worth considering a library solution. I think there's stronger argument for this being a member function than a free one (think analogy to 'contains' and 'at'), but *that* there would be a common, better solution for a widely used access pattern is more important to me than how that solution is spelled.
I'm interested in polling the source code of various projects that use std::map to see how much code this would actually affect. If you have suggestions for what to look at, I'm all ears.
Thanks again,
Kyle
>> On Jul 28, 2021, at 1:40 PM, Kyle Knoepfel <kyleknoepfel_at_[hidden]> wrote:
>>
>> Hi all,
>>
>> C++20 added the std::set::contains and std::map::contains member functions, which perform the equivalent of a find, whose return value is then compared with the appropriate end iterator, resulting in Boolean true or false. We therefore have the nice cleanup:
>>
>> Pre-C++20: if (set.find(key) != cend(map)) { /* key is present */ }
>> Post-C++20: if (set.contains(key)) { /* key is present */ }
>>
>> For std::map, however, there seems to be less utility for 'contains' as the key's corresponding value is often required if the key is present in the map. For example, this is a common usage pattern:
>>
>> std::map<std::string, unsigned> ages = …;
>> if (auto it = ages.find(name); it != cend(ages)) {
>> std::cout << "Age of " << name << ": " << it->second << '\n';
>> } else {
>> std::cout << name << " not known.\n";
>> }
>>
>> Note all the boilerplate to get to the possibly present value (it->second). In such a case above, 'contains' does not help:
>>
>> std::map<std::string, unsigned> ages = …;
>> if (ages.contains(key)) {
>> std::cout
>> << "Age of " << name << ": "
>> << ages.at(key) // Unnecessarily expensive for value guaranteed to be present
>> << '\n';
>> } else {
>> std::cout << key << " not known.\n";
>> }
>>
>> This situation can be made simpler using an interface with std::optional-like semantics:
>>
>> std::map<std::string, unsigned> ages = …;
>> if (auto age = ages.value_for(name)) {
>> std::cout << "Age of " << name << ": " << *age << '\n';
>> } else {
>> std::cout << name << " not known.\n";
>> }
>>
>> where the type of age is a handle-like class that refers to the value of the 'name' entry (not quite like std::optional as we don't want to copy the actual value).
>>
>> Do others find this idea attractive, modulo bike-shedding issues like the function name?
>>
>> Thanks,
>> Kyle
>
Although I would prefer optional<T&> to be a supported construct (I tend to agree with Barry re. its usefulness), I do not want such discussions to distract from the utility I'm suggesting here. To that end, whether the function returns a bare pointer instead of optional<T&> is secondary to whether there is enough semantic improvement to warrant a new member function at all.
Responding to a few comments:
From Andrey:
> Does this feature extend to multimap/unordered_multimap? If so, the
> returned type would be some sort of a range that could be tested for
> being empty via a contextual conversion to bool. That would also work
> for map/unordered_map.
I don't think this proposal applies well to multimaps. The proposed utility is a non-throwing version of std::map::at in the case of an absent key. Perhaps a better name for it would be std::map::ptr_at. For multimaps, the equal_range member function seems adequate.
From Tony:
> Do you also want to be able to change the value being referenced?
>
> if (auto age = ages.value_for(name)) {
> age++; // it's a new year!
> } else {
> std::cout << name << "not known\n:";
> }
For a non-const ages, yes. To use your example, the point is to make the following replacement:
Current: if (auto it = ages.find(name); it != end(ages)) { ++it->second; }
Proposed: if (auto age = ages.find(name)) { ++*age; }
From Jason:
> The difference in lines here is:
>
> Current:
> if(auto it = ages.find(name); it != ages.end())
>
> Proposed:
> if(auto age = ages.value_for(name)))
>
> So, you're talking about a difference of twelve characters. Not
> exactly what I would call "boilerplate".
You're missing the 'it->second' vs. '*age' access pattern, which is perhaps the primary motivator of this proposal. And number of characters, although important, is subservient to the potential semantic improvement of a proposed utility. For example:
C++17: if (ages.find(name) != cend(ages)) { /* name present */ }
C++20: if (ages.contains(name)) { /* name present */ }
We're not talking about a lot of characters difference here, but providing the 'contains' function was a substantial-enough semantic improvement that the standards committee went for it. The question is whether adding a 'ptr_at' or 'value_for' member function provides similar semantic improvement. I'm not sure yet.
> Also, it normalizes practice that is somewhat dubious. Indeed, we
> added special variable declaration syntax to C++17 specifically to
> *discourage* this kind of thing.
Hmm, the main reason for adding an optional init statement that precedes the if condition was to limit the scope of variables that do not belong outside of if...else code. So although this is a related issue, I'm not sure it is directly germane to the facility being proposed here.
From Zhihao, in response to Jason:
> I wouldn't say the second form
> is discouraged. But it does
> have a drawback: what if you only
> want the false branch?
If you want the false branch of whether an item is present in the map, then you have no need of the (nonexistent) value and you would call 'if (not ages.contains(name))' instead.
From Arthur:
> ...Which goes to illustrate what I think is my point:
> helper methods are super useful but also have way too many knobs to be
> worth standardizing.
That is the correct default response, Arthur. Free functions like you presented should generally be favored. However, when a particular coding pattern is (a) prevalent among worldwide (e.g.) std::map users, and when (b) its access pattern could be improved in terms of readability without degrading performance, it's worth considering a library solution. I think there's stronger argument for this being a member function than a free one (think analogy to 'contains' and 'at'), but *that* there would be a common, better solution for a widely used access pattern is more important to me than how that solution is spelled.
I'm interested in polling the source code of various projects that use std::map to see how much code this would actually affect. If you have suggestions for what to look at, I'm all ears.
Thanks again,
Kyle
>> On Jul 28, 2021, at 1:40 PM, Kyle Knoepfel <kyleknoepfel_at_[hidden]> wrote:
>>
>> Hi all,
>>
>> C++20 added the std::set::contains and std::map::contains member functions, which perform the equivalent of a find, whose return value is then compared with the appropriate end iterator, resulting in Boolean true or false. We therefore have the nice cleanup:
>>
>> Pre-C++20: if (set.find(key) != cend(map)) { /* key is present */ }
>> Post-C++20: if (set.contains(key)) { /* key is present */ }
>>
>> For std::map, however, there seems to be less utility for 'contains' as the key's corresponding value is often required if the key is present in the map. For example, this is a common usage pattern:
>>
>> std::map<std::string, unsigned> ages = …;
>> if (auto it = ages.find(name); it != cend(ages)) {
>> std::cout << "Age of " << name << ": " << it->second << '\n';
>> } else {
>> std::cout << name << " not known.\n";
>> }
>>
>> Note all the boilerplate to get to the possibly present value (it->second). In such a case above, 'contains' does not help:
>>
>> std::map<std::string, unsigned> ages = …;
>> if (ages.contains(key)) {
>> std::cout
>> << "Age of " << name << ": "
>> << ages.at(key) // Unnecessarily expensive for value guaranteed to be present
>> << '\n';
>> } else {
>> std::cout << key << " not known.\n";
>> }
>>
>> This situation can be made simpler using an interface with std::optional-like semantics:
>>
>> std::map<std::string, unsigned> ages = …;
>> if (auto age = ages.value_for(name)) {
>> std::cout << "Age of " << name << ": " << *age << '\n';
>> } else {
>> std::cout << name << " not known.\n";
>> }
>>
>> where the type of age is a handle-like class that refers to the value of the 'name' entry (not quite like std::optional as we don't want to copy the actual value).
>>
>> Do others find this idea attractive, modulo bike-shedding issues like the function name?
>>
>> Thanks,
>> Kyle
>
Received on 2021-07-29 10:26:14