15

Look at this code:

dic:=TObjectDictionary<Integer, TObject>.Create([doOwnsValues]);
testObject:=TObject.Create;
dic.AddOrSetValue(1,testObject);
dic.AddOrSetValue(1,testObject);

The code

  1. Creates a Dictionary that owns the contained values
  2. Adds a value
  3. Adds the same value again, using the same key

The surprising thing is that the object is freed when you add it the second time.

Is this intended behaviour? Or a bug in the Delphi libraries?

The documentation simply says "If the object is owned, when the entry is removed from the dictionary, the key and/or value is freed". So it seems a little odd to Free an object that I have just asked it to Add!

Is there any way to tell the TObjectDictionary to not do this? Currently, each time I add a value I have to check first if that Key-Value combination is already in the Dictionary.

Delphi 2010

[EDIT: After reading all the comments:

My conclusions (for what they are worth)]

  • This seems to be the intended behaviour
  • There is no way of modifying this behaviour
  • Don't use TObjectDictionary (or any of the other similar classes) for anything other than the common "Add these objects to the container. Leave them there. Do some stuff. Free the container and all the objects you added" usage. If you are doing anything more complicated, it's better to manage the objects yourself.
  • The behaviour is poorly documented and you should read the source if you want to really know what's going on

[/EDIT]

awmross
  • 3,789
  • 3
  • 38
  • 51
  • @everyone Sorry I missed all the arguments.. I was asleep in another time zone ;-) – awmross Aug 02 '11 at 23:44
  • How inconsiderate of yours. ;-) – Rudy Velthuis Aug 03 '11 at 00:04
  • why don't you just stop trying to add the same item again and again? – David Heffernan Aug 03 '11 at 00:10
  • That's what I did. "Each time I add a value I have to check first if it's already in the Dictionary" – awmross Aug 03 '11 at 00:14
  • "That's what I did. "Each time I add a value I have to check first if it's already in the Dictionary" : Not sure I understand - the behavior you described should only occur if you overwrite the same key with same value - so you don't have to check the values each time, just maintain unique keys if you must add the same object as a value into different keys. – Vector Aug 03 '11 at 17:00

6 Answers6

9

TObjectDictionary<TKey,TValue> is in fact just a TDictionary<TKey,TValue> which has some extra code in the KeyNotify and ValueNotify methods:

procedure TObjectDictionary<TKey,TValue>.ValueNotify(const Value: TValue; 
  Action: TCollectionNotification);
begin
  inherited;
  if (Action = cnRemoved) and (doOwnsValues in FOwnerships) then
    PObject(@Value)^.Free;
end;

This is, IMO, a rather simple minded approach, but in the ValueNotify method, it is impossible to tell for which key this is, so it simply frees the "old" value (there is no way to check if this value is set for the same key).

You can either write your own class (which is not trivial), deriving from TDictionary<K,V>, or simply not use doOwnsValues. You can also write a simple wrapper, e.g. TValueOwningDictionary<K,V> that uses TDictionary<K,V> to do the brunt of the work, but handles the ownership issues itself. I guess I would do the latter.

Rudy Velthuis
  • 28,387
  • 5
  • 46
  • 94
7

Thats because with reusing the key youre replacing the object and since the dictionary owns the object it frees the old one. Dictionary doesn't compare the value, only key, so it doesn't detect that the value (object) is same. Not a bug, as designed (IOW user error).

On second thought - perhaps the designer of the dict should have taken more care to have both doOwnsValues and AddOrSetValue()... one can argue both ways... I suggest you file it in QC, but I wouldn't hold my breath - it has been so now in at least two releases so it's unlikely to change.

ain
  • 22,394
  • 3
  • 54
  • 74
  • 3
    But that is bad coding. It should do something like: `if doOwnsvalues in WhateverItIsCalled and (oldObject <> newObject) then ...`, i.e. it should safeguard against freeing the old value if it is the same. – Rudy Velthuis Aug 02 '11 at 01:41
  • 2
    I'm not sure I agree. The dict data structure is for mapping keys to values - the dict only cares about keys, values are user responsibility. Rather I would argue that giving over the ownership of the values to the dict is bad design. – ain Aug 02 '11 at 01:47
  • @Rudy That's what I assumed it would do. It should just be one pointer comparison. – awmross Aug 02 '11 at 01:51
  • 4
    @ain: If it is only for mapping, it should not have a doOwnsValues capability. TDictionary is for mapping only. If a TObjectDictionary (or any other class) owns the items it contains, it should check if it is not freeing the item it is assigning. – Rudy Velthuis Aug 02 '11 at 01:59
  • @ain: And it is certainly not a user error. The user can expect the dictionary to take care of the value it owns, and not to free it prematurely. This is a design error, IMO. – Rudy Velthuis Aug 02 '11 at 02:04
  • @Rudy Yeah, I kind of agree that it is bad design, but there is no ISO standard for dict so who is to say what is right and what is wrong. It is matter of opinion and the current behaviour can be said to be limitation of the implementation... adding same key / value pair into dict isn't good design either. – ain Aug 02 '11 at 02:12
  • There is no standard, and so, de facto, the implementation is the standard, indeed, but this is sloppy design, IMO. Adding the same key/value can happen, especially if, at compile time, you don't know what keys or values to expect. A good dictionary would handle this gracefully. And how a proper dictionary owning objects works can be seen on the Mac, i.e. NSDictionary. – Rudy Velthuis Aug 02 '11 at 02:25
  • I can see reasons for it behaving this way (as Rudy pointed out in his answer, it makes the implementation very simple). Not that I *agree* with those reasons. I just wish for some *documentation* of the *exact* behaviour, instead of leaving it up to the programmer to guess, post on forums or trawl through sources. Delphi docs are often annoyingly brief and vague :-( – awmross Aug 02 '11 at 02:34
  • 2
    @awmross It is what it is. Just get used to reading the sources. That will bring lots of side benefits since you'll discover lots of capability that you did not know existed. Well, I do when I read the sources! – David Heffernan Aug 02 '11 at 07:13
  • 2
    @David ok, but if there is no clear specification for it, nobody can tell if the source is a correct implementation of the defined behavior. It is just what it is. – mjn Aug 02 '11 at 10:06
  • "Nobody can tell if the source is a correct implementation of the defined behavior". The source is, for all code, the only true specification of behaviour. – David Heffernan Aug 02 '11 at 10:13
  • 2
    @David so if all source code is its own specification, there can be no incorrect code, right? :) – mjn Aug 02 '11 at 12:27
  • @mjn When there is not specification, the implementation is all you have. – David Heffernan Aug 02 '11 at 12:47
5

This behaviour is by design and the design is sound.

Were the class to take responsibility for not freeing duplicates, it would have to iterate over the whole container every time a modification was made, both adding and removing. The iteration would check for any duplicate values and check accordingly.

It would be disasterous to impose this diabolical performance drain on all users of the class. If you wish to put duplicates in the list then you will have to come up with a bespoke lifetime management policy that suits your specific needs. In this case it is unreasonable to expect the general purpose container to support your particular usage pattern.


In the comments to this answer, and many of the others, it has been suggested that a better design would have been to test in AddOrSetValue whether or not the value being set was already assigned to the specified key. If so, then AddOrSetValue could return immediately.

I think it's clear to anyone that checking for duplicates in full generality is too expensive to contemplate. However, I contend that there are good design reasons why checking for duplicate K and V in AddOrSetValue would also be poor design.

Remember that TObjectDictionary<K,V> is derived from TDictionary<K,V>. For the more general class, comparing equality of V is potentially an expensive operation because we have no constraints on what V is, it being generic. So for TDictionary<K,V> there are performance reasons why we should not include the putative AddOrSetValue test.

It could be argued that we make a special exception for TObjectDictionary<K,V>. That would certainly be possible. It would require a little re-engineering of the coupling between the two classes, but it is quite feasible. But now you have a situation where TDictionary<K,V> and TObjectDictionary<K,V> have different semantics. This is a clear downside and must be weighed against the potential benefit from the AddOrSetValue test.

These generic container classes are so fundamental that design decisions have to take into account a huge spread of use cases, consistency considerations and so on. It is not, in my view, reasonable to consider TObjectDictionary<K,V>.AddOrSetValue in isolation.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 4
    The design is, IMO, terribly unsound. The proper deisgn only has to check if, for that given key, which it must search anyway, the new value is not the same as the old value. It must retrieve it anyway, so this is a no-brainer and does not require any extra iteration. There is also nothing wrong with two keys having the same value, e.g. age=30, weight=110, numberofCats=30, etc. But there is something wrong with freeing the item you want to add. – Rudy Velthuis Aug 02 '11 at 06:32
  • 3
    @rudy Why only check the key being added? What if the object is already owned by a different key? Your thinking is clouded by the OP's very specific use case. Designers have to consider generality. – David Heffernan Aug 02 '11 at 06:49
  • 1
    If the object is already owned by a _different_ key, then it is a user error, IMO. A `TObjectDictionary` is meant to link pairs of items together. Changing the value for a key is not unusal, but freeing the value if it is simply added again is. I am not expecting the dict to check all entries. It should, however, check the newly added item before freeing the "old" one. FWIW, if the user must check for duplication, what is the use of a dictionary? – Rudy Velthuis Aug 02 '11 at 06:54
  • @rudy You want the dict to check for some user errors but not all? I want my containers not to check for user error. I strive not to commit such errors and don't see why I should pay the price for other users' errors. – David Heffernan Aug 02 '11 at 06:59
  • 4
    I don't want the dict to check for user errors, but adding the same item to the same key twice is **not a user error**, and the dict should handle it gracefully. It should be possible to do it as often as required. – Rudy Velthuis Aug 02 '11 at 07:34
  • @Rudy If you want such a check, add it yourself. Why should my code have to pay that price too? – David Heffernan Aug 02 '11 at 07:43
  • @Rudy Another realisation dawned with me concerning this most interesting debate—see my edit. – David Heffernan Aug 02 '11 at 23:00
  • 1
    The semantics are already very different: `TObjectXXX` classes **own** their items and invalidate them if they release them. `TDictionary` merely creates a mapping between `K`'s and `V`'s, so there, that risk is not there, it doesn't invalidate its items. I think that an **owning** class should take care not to free the item it is adding. Even `TObjectList` should do that (but, AFAICT, doesn't either). – Rudy Velthuis Aug 02 '11 at 23:12
  • @Rudy I think you are too blinkered. I think you are looking at this from the perspective of one user with one problem, and not from the viewpoint of the designer. And as for the semantics being different, I'm not so sure. For `TDictionary`, `V` is typically not an object and typically has value type semantics. So it is owned by the dictionary, and freed by it. – David Heffernan Aug 02 '11 at 23:16
  • 1
    I may be too blinkered, but I am looking at this from a global perspective. I don't think an owning class should invalidate items it is still storing. I don't care what kind of container that owning class is. Of course it can't prevent the **user** from freeing such an item, but that is the user's responsibility. The owned objects are the container's responsibility, and to ensure it own integrity, it should not free items it is still containing. A user passing such an object does not expect the container to destroy it immediately, no matter how often it is added to the container. – Rudy Velthuis Aug 02 '11 at 23:24
  • @Rudy Make your mind up. You argue the exact opposite in a comment above. You argue that containers should indeed free items that they contain. You state "I am not expecting the dict to check all entries". That effectively says that it is OK for the container to free an item that it contains. – David Heffernan Aug 02 '11 at 23:28
  • They should free the items they **contained** (past tense), when they are finished with them (i.e. are removing them finally), or when they themselves are freed. They should not remove and free an item directly before they add it again, especially not **in the method that is supposed to _add_ it**, i.e. is supposed to safeguard it. A simple check should avoid that. FWIW, in a `TDictionary` I can add the same value (even an object) for the same key multiple times without any bad side effects. It is only stored once for that key, of course. – Rudy Velthuis Aug 02 '11 at 23:35
  • @Rudy Add the same value to `TDictionary` without bad effects? Well of course not, they don't take ownership! Again, you have blinkers on and can't see the bigger picture, in my view. – David Heffernan Aug 02 '11 at 23:39
  • I think I explained my view, that a container should not free an item it is supposed to keep safe as long as it has ownership and I don't think I have "blinkers" (blinders) on at all. Let's agree to disagree, since this is not leading us anywhere. I think I will make this a topic in the Embarcadero forums and ask for opinions there. That palce is used to very long discussions, especially with me in them. ;-D – Rudy Velthuis Aug 02 '11 at 23:43
  • @rudy you argued two contradictory positions. Did you recant the view you held about duplicates? Do you know believe that the dict should block duplicate values? – David Heffernan Aug 02 '11 at 23:48
  • I don't have conflicting views on this at all. As I said, I am happy to discuss this is in a more suitable forum, like Embarcadero's. See you there, if you wish. You're welcome. – Rudy Velthuis Aug 02 '11 at 23:55
  • @David You argued that adding an object twice is an error. You also argued that TObjectDictionary and TDictionary should have the same semantics. You are now arguing that TDictionary *can* have duplicates, because it doesn't "take ownership", but TDictionary *can't*. You are arguing two contradictory postions. – awmross Aug 02 '11 at 23:59
  • @rudy more suitable forum? What's wrong with here? Anyway, feel free to let me know how you resolved the contradiction. – David Heffernan Aug 02 '11 at 23:59
  • TDictionary typically holds value types and the whole issue of duplicate references is not meaningful. My main objection is that your proposed change creates different treatment for owned duplicates depending on whether or not they have the same or different keys. But I'm beginning to lose track of the arguments and doubt my reasoning now. – David Heffernan Aug 03 '11 at 00:09
  • @David: The Embarcadero forums are **discussion** forums. This forum, StackOverflow, is meant to give solutions for well defined answers. It is not really a discussion forum, as the Embarcadero forums are. Comments are not suitable for long winding threads. And ping-pong threads like ours (you write, I reply, you reply, etc...) are a nuisance everywhere. – Rudy Velthuis Aug 03 '11 at 00:57
  • @rudy maybe but I'll never hang out on emba forums. Can you imagine how much time I have left after my stack overflow addiction?! – David Heffernan Aug 03 '11 at 01:02
  • @David: LOL! But I hang out there regularly. I am a member of their TeamB, i.e. I'm their quivalent of a Microsoft MVP, but with moderator status. I don't play golf, so I have some spare time left. – Rudy Velthuis Aug 03 '11 at 01:05
2

Since the Delphi TDictionary implementation doesn't allow for more than one of the same keys you could check the excellent Generic collections library from Alex Ciobanu. It comes with a TMultiMap or for your case TObjectMultiMap that allows for multiple values per key.

Edit: If you don't want multiple values per key, but rather want to avoid adding duplicates to the Dictionary then you can try TDistinctMultiMap or a TObjectDistinctMultiMap from the same Collections library.

iamjoosy
  • 3,299
  • 20
  • 30
  • OP doesn't want multiple values per key – David Heffernan Aug 02 '11 at 08:51
  • @David, I disagree, OP wants multiple values per key, although the values might be the same. – iamjoosy Aug 02 '11 at 09:06
  • If OP really does want multiple values per key then your answer is correct. I think OP wants something like `dupIgnore` in `TStringList`. – David Heffernan Aug 02 '11 at 10:11
  • @David, having reread the question again, you might be right and in fact the OP wants something similar to dupIgnore. Maybe the OP can clarify – iamjoosy Aug 02 '11 at 10:21
  • This time I agree with David. He wants to replace the value for a key with the same value. – Rudy Velthuis Aug 02 '11 at 10:56
  • @Rudy There's a first time for everything!! ;-) – David Heffernan Aug 02 '11 at 13:04
  • 1
    @David: I often agree with you. Guess who gave you all those upvotes? – Rudy Velthuis Aug 02 '11 at 13:11
  • David is very prolific here and adds a great deal to the Delphi tagged discussions - one can often disagree with him, but his opinion always needs to be weighed carefully - most of the time he indeed knows what he's talking about...LOL – Vector Aug 02 '11 at 13:40
  • @Everyone I just want to be able to add the same value to the Dictionary without it being freed! In other words, I would expect the second call to AddOrSetValue to have no effect. Doesn't sound like too much to ask :-) – awmross Aug 02 '11 at 23:22
0

I think it is a bug. I ran into it a week ago.

I use the TObjectDictionary for storing some real time telemetria datas, which are very often updated with new datas.

for example:

Type TTag = class
               updatetime : TDateTime;
               Value      : string ;
            end ;

TTagDictionary:= TObjectDictionary<string,TTag>.Create([doOwnsValues]);


procedure UpdateTags(key: string; newValue: String) ;
var 
   tag : TTag ;
begin
     if TTagDictionary.TryGetValue(key,tag) then begin  // update the stored tag
        tag.Value = newValue ;
        tag.updatetime := now ;
        TTagDictionary.AddorSetValue(key,tag) ;
     else begin
        tag := TTag.Create ;
        tag.updatetime := now ;
        tag.Vluae := newValue ;
        TTagDictionary.AddorSetValue(key,tag) ;
     end ;
end ;

After several updates I ended up with some nasty access violations and with an dictionary full of freed objects.

It is a very poor designed container.

At update it need check if the new object is the same as the old only and then it must NOT free the object.

0

So it seems a little odd to Free an object that I have just asked it to Add!

You didn't ask the dictionary to add - you called 'AddorSet', and since the key was already found, your call was a 'set', not an 'add'. Regardless, I see nothing odd here in terms of Delphi's behavior: In Delphi, objects are only object references, and there is no reference counting or ownership for simple objects.

Since in this case the dictionary owns the objects, it is doing exactly what it's supposed to do: "If the object is owned, when the entry is removed from the dictionary, the key and/or value is freed". You removed the value when you overwrote entry[1] - therefore the object referred to in 'testObject' is immediately deleted and your reference to 'testObject' is invalid.

Currently, each time I add a value I have to check first if it's already in the Dictionary.

Why is that? The behavior you described should only occur if you overwrite a previously used key with a reference to the same object.


Edit:

Perhaps there is something 'odd' after all - try this test code:

        procedure testObjectList;
        var ol:TObjectList;
            o,o1:TObject;
        begin

          ol:=TObjectList.create;
          ol.OwnsObjects:=true;//default behavior, not really necessary
          try
            o:=TObject.create;      
            ol.add(o);
            ol[0]:=o;
            showmessage(o.ClassName);//no av-although ol[0] is overwritten with o again, o is not deleted
            o1:=TObject.create;
            ol[0]:=o1;
            showmessage(o.ClassName);//av - when o is overwritten with o1, o is deleted
          finally
            ol.free
          end;

        end;

This in spite of what it says in the (Delphi 7) help: "TObjectList controls the memory of its objects, freeing an object when its index is reassigned"

Vector
  • 10,879
  • 12
  • 61
  • 101
  • 1
    I see something very odd here. If it is added twice, lots of valid things could happen (silent refusal, exception, etc.), but simply freeing it should not be one of them. There is nothing to defend this, it is clearly a bug. – Rudy Velthuis Aug 02 '11 at 06:28
  • David has already answered your contentions Rudy - I don't think it's asking too much from a developer not to overwrite the same key with the same value. – Vector Aug 02 '11 at 07:03
  • Also, even if agreed that it was a poor (which I don't) design, it's certainly not 'odd' considering the way Delphi handles object references - there is no dearth of ways to shoot yourself in the foot if you don't pay attention. – Vector Aug 02 '11 at 07:09
  • 2
    Freeing an item that is just being added is certainly poor design, and poor design ought to be odd. I understand why it happens, and that it is not so easy to change it, but it should not happen. I do think it is asking something of the user that the class can and should do. If you add duplicates to any of the other container classes, you get an error (exception), or it is silently refused, but the item is not invalidated in any way. This is behaviour no expects or should have to expect. – Rudy Velthuis Aug 02 '11 at 07:30
  • @Rudy - "f you add duplicates to any of the other container classes" - try this with TOBjectList, which owns objects by default. I haven't tried now (it's VERY LATE here...) but I'm pretty certain you'll see the same behavior. IMO Keeping track of object lifetimes is not the job of this type of container class, it's the job of the developer, at least in the Delphi universe (I'm just repeating what David said now ) Besides, this isn't really adding, it's overwriting a previous key **(addOrSet)** So I guess we are just going to have to agree that we disagree... Cheers. – Vector Aug 02 '11 at 07:57
  • @Rudy You are not being consistent. Now you are talking about duplicates and saying that all containers should either raise an error or silently refuse them. But in comments to my answer you say the opposite, that adding duplicates is user error. – David Heffernan Aug 02 '11 at 07:58
  • @Mikey You are correct. Add an object twice to an owning `TObjectList`. Then deleting them leads to a double free. – David Heffernan Aug 02 '11 at 07:59
  • If the developer picks a class that owns objects, the developer trusts the class to take care of the objects, and not to free them prematurely. I don't have Delphi here, but I'll check the other classes as soon as I can. I don't think any of them frees an item prematurely, e.g. `TObjectList.SetItem` or similar. – Rudy Velthuis Aug 02 '11 at 08:45
  • @Rudy As I already said, a `TObjectList` that owns objects does a double free when there are duplicates. – David Heffernan Aug 02 '11 at 09:44
  • Re: update, my belief is that RTL designers view adding duplicates to be an error and outside the specification of the container classes. – David Heffernan Aug 02 '11 at 18:12
  • @David - we're not talking about a double free - check out the code I put into my edit here, which is analagous to the code in the OP - TObjectList SEEMS to behave the way the OP and Rudy expect - I haven't traced into the VCL.. – Vector Aug 02 '11 at 22:32
  • @Mikey `TList.Put` contains this line: `if Item <> FList^[Index] then` but the double free I'm thinking of is if you add the same item twice. – David Heffernan Aug 02 '11 at 22:45
  • @Mikey The equivalent test is not present in `TList.SetItem`, I believe for reasons outlined in my updated answer. – David Heffernan Aug 02 '11 at 23:01
  • @David If RTL designers have a "belief" that adding duplicates is an "error" then perhaps they could.. you know.. share that "belief" with us humble Delphi developers? I don't see anywhere in the docs that "adding the same object twice to a collection is an error". Besides, I've never seen a definition of List or Collection in any language that included "Cannot contain duplicate items" (A Set can't contain duplicates, but even then trying to add a duplicate isn't usually an error) – awmross Aug 02 '11 at 23:42
  • @awmross I did not write nor document the RTL. If you don't like it, complain to those that did. I'm just giving my time trying to help you understand why it is what it is. I believe that the new generic containers are by and large excellent. I've explained in my answer why adding duplicates to owning lists has to be treated as user error. You'd really hate the performance if it was otherwise. – David Heffernan Aug 02 '11 at 23:54
  • @Mikey re: "Why is that? The behavior you described should only occur if you overwrite a previously used key with a reference to the same object" That's what I meant. I'll edit my question. – awmross Aug 03 '11 at 00:03
  • @David I gave your answer an upvote :-) Regardless, this is a straw man argument. You are presenting one potential design that happens to have terrible performance, and then concluding that the only possible alternative is the (flawed) design we have been given. There are countless other possible implementations. I understand the design tradeoffs that led to the chosen implemenation. All I am suggesting is that perhaps they could document the behaviour. For whatever reason you find this an outrageous suggestion. – awmross Aug 03 '11 at 00:12
  • @awmross No, I'd love some better documentation. I'm with you on that. I just don't think it will happen and you have to respect that the emba devs have limited resources as do everyone and maybe expanding documentation gets lower priority than things like 64 bit!o everyone and maybe expanding documentation gets lower priority than things like 64 bit! – David Heffernan Aug 03 '11 at 00:16
  • As for straw man I take your point. If I was designing it, once I'd concluded that it wasn't possible to stop all user duplicate errors in reasonable time, I personally would not attempt to stop any duplicate errors. I think that's a sound position, and I think other positions are sound too. I don't believe it is as clear cut as Rudy argues and I expect emba would respond to any qc report with as designed, won't fix. – David Heffernan Aug 03 '11 at 00:21