6 ms·
I find it so strange and unintuitive that a table lookup in STL can actually amend the table to include the key. I don't know of another library that works like
by colonwqbang 4y ago
I find it so strange and unintuitive that a table lookup in STL can actually amend the table to include the key. I don't know of another library that works like that.
std::map<std::string, std::string> m;
(void) m["nonexistent"];
m.size(); // => 1
Is there a historic reason for this strange specification? Or is it just a concession make the "operator=" sugar work better?
- cmovq 4y agoIf you try to implement your own map you’ll naturally end up with this quirk. operator[] returns a reference to a slot in the map which can be read from or assigned to. Since the reference must be valid, you better have allocated a slot in the map that can be written to if the value didn’t exist.
- madars 4y agoI don't think it's _that_ unnatural to just return a reference to a dummy value (i.e. have a member T map<T>::dummy and always return that if the key does not exist). Users writing to it/reading from it are on their own; you could even reinitialize it on every such operator[] call.
- cmovq 4y agooperator[] is not only used for lookups though, it’s also used for insertions: m["nonexistent"] = 1; This would not work if you return a dummy value.
- madars 4y agoThat's true; thanks! It's 3am and time for bed :-)
- 8n4vidtmkvmk 4y agoThis is why I gave up on [] and just use .at now.
- vitus 4y agoWhich is perfectly reasonable if you're willing to deal with the performance consequences of exceptions (or crashes). .find() is the safe way to do map lookup without side effects.
- colonwqbang 4y agoIt is a quirk of C++ that element lookup and assignment are forcibly unified like this. Ruby for instance has an operator[]= which could solve the problem.
- gpderetta 4y agoYou could do it in C++ via proxy objects, but are kind of tricky to get right.
- raverbashing 4y ago"quirk" looks more like "major overlooked detail" to me I mean, left side expressions and right side expressions are different
- tialaramex 4y agoIndeed, Rust's HashMap implements Index<&T> but not IndexMut<&T> meaning you can write this: let salary = salaries["Jim Bob"]; // Note this panics if "Jim Bob" isn't a key in salaries, by analogy to arrays which panic if you do an out-of-bounds array access However you can't write this, it won't compile: salaries["Jim Bob"] += 10_000; // No raise for Jim Bob, this does not compile because that's not a mutable reference.
- deredede 4y ago
- masklinn 4y agoThere are two issues with that: - what is the dummy value? does the value type even have one? - I don't think index-assignment (or attribute-assignment) is overloadable in C++, so you need `a[b]` to return something which can be assigned to, and proxy values tend to be really wonky (see: `vector<bool>`), so assigning to the result of the indexing would replace the dummy value
- vlovich123 4y agoThis was written at a time when std::option didn't exist. Thus it needed to return T& instead of option<T&>. When std::option was added, they couldn't change the existing interface because of back compat and the lack of a migrator tool (e.g. Swift has major backwards incompatible source changes but they have a tool that will migrate your code fore you). C++ has warts like this all over the place.
- cmovq 4y agooptional<T &> is not valid in C++ [1] [1]: https://en.cppreference.com/w/cpp/utility/optional https://en.cppreference.com/w/cpp/utility/optional
- tialaramex 4y agoBecause C++ doesn't have the Guaranteed Niche Optimization C++ optional<T&> would be pretty miserable whereas Rust's Option<&T> is exactly what you want. In practice C++ APIs where you'd naturally write Option<&T> use pointers and are consequently easier to use wrong, this is an example of Rust actually providing improved safety technology (whereas many commonly identified safety differences like bounds checking in Rust are cultural differences)
- gpderetta 4y agoC++ could specify that std::optional<T&> is valid and implementations could implement the Guaranteed Niche Optimizations. This is not a theoretical possibility: boost.optional (which std::optional is based on) does exactly that. The reason std::optional doesn't allow references is because someone in the commitee objected that some users might be confused by the assignment semantics (will assignment rebind or assign through the reference?) and the proposal authors dropped it instead of spending too long discussing pro and cons. It can still be easily re-added if the commitee wants to. No need to drag the rust in the discussion.
- tialaramex 4y ago
- camel-cdr 4y agoIsn't that pretty common? The [] operator acts like find or insert default value, so you can do nice things like ++map[key]. The map.find(key) works if you don't want to insert, it will return map.end() if the key couldn't be found.
- deredede 4y agoMy only gripe with find() is that you can't do `if (auto it = map.find(key))`. Otherwise yes, that's the way.
- masklinn 4y ago> Isn't that pretty common? AFAIK basically just 3 wide-spread languages which do this, and they're all known for their idiosyncratic behaviour: C++, Perl, and PHP. A few languages provide this as opt-in via a subtype (Python's defaultdict) or init (Ruby's Hash.new with a block parameter). Some more will do it via an opt-in callsite API e.g. Python's `setdefault`, Java's `computeIfAbsent`, Rust's `entry` API.
- kragen 4y agoperl only does this in some cases; unlike with std::map, this is safe: my $x = $h{$v}; this is not safe because it autovivifies $h{$v} as an ARRAY ref: my $x = $h{$v}[$i]; with respect to idiosyncratic behavior, if a language doesn't have idiosyncratic behavior, it has no reason to exist; it's just gratuitously incompatible syntax for already-available semantics
- MatmaRex 4y agoPython's defaultdict also behaves like this, inserting the nonexistent entry into the dict: from collections import defaultdict m = defaultdict(int) m["nonexistent"] len(m) # => 1 There are examples of the opposite too, though. Ruby's Hash does not insert the entry: m = Hash.new(0) m["nonexistent"] m.length # => 0
- lifthrasiir 4y agoPython's defaultdict is aptly named to indicate this behavior (the bare `dict` won't do this). `std::map` isn't.
- masklinn 4y ago> There are examples of the opposite too, though. Ruby's Hash does not insert the entry: It does if you initialise the hash with a block to do so: m = Hash.new {|h, k| h[k] = 0} m["nonexistent"] m.length # => 1 That is, essentially, what defaultdict does. Just through subclassing (because `dict` does not provide such a fallback, but it has a magic method you can override to customise the mapping's behaviour in that case).
- kragen 4y agothis famously introduced bugs into norvig's published code (i forget if it was the sudoku solver or the spell checker) that nobody noticed for years if norvig isn't smart enough to use defaultdict without introducing bugs then i'm not either
- abecedarius 4y agoiirc it was the spellchecker. My memory is fuzzy and unreliable but what it's claiming is that the first version on his webpage had that bug, he fixed it, and later I pointed out another bug of the same type. Or something like that! (I am sure I never reported anything about the sudoku solver, since I never studied it.)
- kragen 4y agothanks!
- lionkor 4y agoLookup is map.at() (throws on invalid element), you can use map.contains() since c++20 or map.find()!=map.end() to see if the element exists beforehand. operator[] does more than lookup, but there are other functions for the other stuff. It should be pretty clear...?
- colonwqbang 4y agoThings can be clear while still being unintuitive :) Comparing to other languages I know, I couldn't think of another where collection[key] denotes insertion.
- planede 4y agooperator[] is indeed not great, the semantics are too surprising for such a short-hand notation. auto [it, newly_created] = m.try_emplace(key); Does the same, and gives back more information. For something like `m[key] = value`, you can do `m.insert_or_assign(key, value)`, and save the default construction + assignment when the key was not there. For ordinary lookup you don't want to randomly mutate the map anyway. If your reference to the map is const, you can't use operator[] anyway, fortunately. However `m.at(key)` is not great either when the key not being present is really not exceptional, and the dance with `m.find(key)` is often not too ergonomic. I still suggest to just use `find` here.
- gpderetta 4y agoI (and many other C++ programmers), on the other hand, I'm always annoyed and surprised that dictionaries on other languages do not have default insert on lookup. It is just very convenient (hence the existence of defaultdict in python).
- deredede 4y agoInsert on lookup is convenient but encourages either double-lookup patterns (contains + get) or unintentional inserts. I very much like Python's setdefault -- except for its 10-chars name.
- gpderetta 4y agoWell, STL encourages an iterator centric approach, so: if(auto i = x.find(y); i!=x.end()){ // use i->{first,second} } Also ideally you should be able to do this: for(auto [key, value]: x.equal_range(y)) { } But infuriatingly, x.equal_range returns a pair instead of some range view so you need some additional infrastructure (either convert the result of equal range into a range or add some begin/end overloads for pair).
- deredede 4y agoMy C++ is a bit rusty (no puns intended), I didn't know your first syntax was possible now (another comment states it comes from C++17), that's much nicer than the alternative that pollutes the scope indeed. Having to repeat both x and i is unfortunate, but that's par for the course with C++ iterators. The equal_range solution looks nice from a genericity perspective (look ma I do multimaps!) but is really unintuitive with regular maps imo. Unless I'm mistaken it also does not allow an else branch for when the value is not there, which is often needed.
- gpderetta 4y agoA for-else a-la Python would actually be generally useful! Hopefully one day we will get proper patter matching and destructing in if statements (Herb Sutter has made significant progress on this).
- meindnoch 4y agoYes. Especially annoying when looking up something in a const& map. The alternative approach could have been to make operator[]= and operator[] different.
- tialaramex 4y agoYou couldn't quite do that, because C++ has compound operators, and they can of course be overloaded, so: foo[bar] += baz; ... needs to provide operator+= with a mutable reference to foo[bar], and a maybe immutable reference to baz.
- int_19h 4y agoSo does C#, and it simply desugars the above into (foo[bar] = foo[bar] + baz), except that "foo" and "bar" are only evaluated once.
- tialaramex 4y agoIn what version of C# do you believe C# gained overloading for the compound assignment operators ? The latest Microsoft documentation I could find claims these operators cannot be overloaded.
- int_19h 4y agoIt was always present. The reason for the claim in the docs is because they cannot be overloaded by themselves, only indirectly via overloading the binary operators that they're syntactic sugar for. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#11193-compound-assignment https://learn.microsoft.com/en-us/dotnet/csharp/language-ref... Which is actually better than C++ in more than one way. As a library author, you overload a+b, and you get a+=b for free. As a user, you know that a+=b always means the same thing as a=a+b (except for side effects) regardless of the library. Similarly, you can't overload && and || in C# directly like you can in C++, but instead you overload "operator true" and "operator false", and you get the boolean ops consistently derived from that (which means that they can short-circuit properly when overloaded, unlike in C++).
- int_19h 4y agoThe historic reason is that in C, [] on arrays is defined as returning an lvalue on which assignment then operates - that is, they have always been separate things, and there was no operator []= or anything like that to define semantics of array assignment in a single operation. C++ simply generalized this model to all collections, in line with the overall "better C" design. But then for maps it means that [] has to proactively create an entry if you want []= to work like it does everywhere else. Similarly, C++ iterators are generalized C pointers, which is also not an ideal abstraction in practice (and why few other languages do it that way).