10

Please take into account my inexperience, but I do not understand the point of std::owner_less.

I have been shown that a map with weak_ptr as key is not recommended because an expired weak_ptr key will break the map, actually:

If it expires, then the container's order is broken, and trying to use the container afterwards will give undefined behaviour.

How undefined is that behavior? The reason I ask is because the docs say about owner_less:

This function object provides owner-based (as opposed to value-based) mixed-type ordering of both std::weak_ptr and std::shared_ptr. The order is such that two smart pointers compare equivalent only if they are both empty or if they both manage the same object, even if the values of the raw pointers obtained by get() are different (e.g. because they point at different subobjects within the same object)

Again, this is my inexperience talking, but it doesn't sound like the map will be completely broken by an expired weak_ptr:

Returns whether the weak_ptr object is either empty or there are no more shared_ptr in the owner group it belongs to.

Expired pointers act as empty weak_ptr objects when locked, and thus can no longer be used to restore an owning shared_ptr.

It sounds like it could become more flabby than completely undefined. If one's implementation removes expired weak_ptrs and simply doesn't or has no use for any lingering ones, when does the behavior become undefined?

If one's implementation has no regard for order, yet only needs a convenient way to associate weak_ptrs with data, is the behavior still undefined? In other words, will find start to return the wrong key?

Map

The only problem that I can find in the docs is what's referenced above, that expired weak_ptrs will return equivalent.

According to these docs, this isn't a problem for implementations that do not rely on ordering nor have use for expired weak_ptrs:

Associative

Elements in associative containers are referenced by their key and not by their absolute position in the container.

Ordered

The elements in the container follow a strict order at all times. All inserted elements are given a position in this order.

Map

Each element associates a key to a mapped value: Keys are meant to identify the elements whose main content is the mapped value.

That sounds like if an implementation is not concerned with order nor has use for expired weak_ptrs then there is no problem because values are referenced by key not by order, so finding an expired weak_ptr will return possibly another weak_ptrs value, but since there's no use for it in this particular implementation except to be erased, there's no problem.

I can see how a need to use weak_ptr ordering or expired weak_ptrs could be a problem, whatever application that may be, but all behavior seems far from undefined, so a map or set does not seem to be totally broken by an expired weak_ptr.

Are there more technical explanations of map, weak_ptr, and owner_less that refute these docs and my interpretation?

Community
  • 1
  • 1
  • 2
    Anything that changes the relative order of two keys in a map will break it in a bad way. – Mark Ransom Apr 22 '14 at 03:02
  • "Associative", "Ordered", "Map" are not implementations, they are conceptual abstractions. – Oktalist Apr 22 '14 at 03:04
  • @Gracchus You didn't link to where you found that quote. – Oktalist Apr 22 '14 at 03:08
  • @Gracchus it all depends on the actual implementation so there are no documents that will be universally applicable. However they're all based on some kind of tree structure, and navigating the tree properly depends on the comparison remaining stable. One possible outcome would be an inability to find an element that was previously inserted in the map. – Mark Ransom Apr 22 '14 at 03:13
  • As a rule, stay away from cplusplus.com. Having said that, I think the specific problem you are having with that quote is misunderstanding what is meant by "referenced by". From the caller's perspective, values are referenced by keys; this says nothing about how values may be managed internally by the container. – Oktalist Apr 22 '14 at 03:15
  • Like Mark said it depends on the implementation. The standard leaves it pretty much unstated, so that implementations are free to pick the most efficient way in their case, which is why the standard must be quite conservative in what it allows here, because it doesn't know exactly what each implementation might depend on. – Oktalist Apr 22 '14 at 03:26

3 Answers3

4

One point of clarification. Expired weak_ptr's are not UB when using owner_less. From the standard

under the equivalence relation defined by operator(), !operator()(a, b) && !operator()(b, a), two shared_ptr or weak_ptr instances are equivalent if and only if they share ownership or are both empty.

One thing to remember is that an empty weak_ptr is one that has never been assigned a valid shared_ptr, or one which has been assigned an empty shared_ptr/weak_ptr. A weak_ptr that has expired is not an empty weak_ptr.

Edit:

The definition above hinges on what does it mean to have an "empty" weak_ptr. So, let's look at the standard

  • constexpr weak_ptr() noexcept;

    Effects: Constructs an empty weak_ptr object.
    Postconditions: use_count() == 0.

  • weak_ptr(const weak_ptr& r) noexcept;
  • template weak_ptr(const weak_ptr& r) noexcept;
  • template weak_ptr(const shared_ptr& r) noexcept;

    Requires: The second and third constructors shall not participate in the overload resolution unless Y* is implicitly convertible to T*.

    Effects: If r is empty, constructs an empty weak_ptr object; otherwise, constructs a weak_ptr object that shares ownership with r and stores a copy of the pointer stored in r.

    Postconditions: use_count() == r.use_count().

Swapping simply exchanges contents, and assignment is defined as the above constructors plus a swap.

To create an empty weak_ptr, you use the default constructor, or pass it a weak_ptr or shared_ptr that is empty. Now, you'll note expiration doesn't actually cause a weak_ptr to become empty. It simply causes it to have a use_count() of zero and expired() to return true. This is because the underlying reference count cannot be released until all the weak pointers that shared the object are also released.

Dave S
  • 20,507
  • 3
  • 48
  • 68
  • 1
    @Gracchus: The quote is correct, but the issue is that your original post (and the one you linked to) are confusing 'expired' vs. 'empty'. Expired means that it once pointed to something, but all of the shared_ptr's have been eliminated. In the expired case, the control structure with the reference count remains. Empty means there is no underlying control structure. An empty weak_ptr will not compare equivalent to an expired weak_ptr. – Dave S Apr 22 '14 at 03:28
  • 2
    @Gracchus: Non-empty `weak_ptr`s will compare equivalent if they share ownership of the same object, even if that object has expired due to the removal of all `shared_ptr`s to that object. `shared_ptr a(new int); weak_ptr b(a); weak_ptr c(a); a.reset(); // b and c are still equivalent` – Dave S Apr 22 '14 at 04:00
  • 1
    @Gracchus: One additional thing I wanted to add to the example `shared_ptr a(new int); weak_ptr b(a); weak_ptr c(a); weak_ptr d; a.reset(); // b and c are still equivalent, but d is not`. See http://ideone.com/A276pv – Dave S Apr 22 '14 at 11:24
  • 2
    @Gracchus _"I think you've explained why my maps, when filled with expired weak_ptrs, do not exhibit undefined behavior"_ <- correct, if and only if you use `owner_less`. – Oktalist Apr 22 '14 at 12:56
  • 2
    @Gracchus `owner_less` compares the owners of the pointers, not the values of the pointers. The value of a `weak_ptr` changes when it expires, but the _owner_ of a `weak_ptr` does _not_ change when it expires, so the ordering induced by `owner_less` is not affected by expiry. – Oktalist Apr 22 '14 at 14:48
3

Here is a minimal example which demonstrates the same problem:

struct Character
{
    char ch;
};

bool globalCaseSensitive = true;

bool operator< (const Character& l, const Character& r)
{
    if (globalCaseSensitive)
        return l.ch < r.ch;
    else
        return std::tolower(l.ch) < std::tolower(r.ch);
}

int main()
{
    std::set<Character> set = { {'a'}, {'B'} };

    globalCaseSensitive = false; // change set ordering => undefined behaviour
}

map and set require that their key comparator implement a strict weak ordering relation over their key type. This means that, among other things, if x is less than y then x is always less than y. If the program does not guarantee that, then the program exhibits undefined behaviour.

We can fix this example by providing a custom comparator which ignores the case sensitivity switch:

struct Compare
{
    bool operator() (const Character& l, const Character& r)
    {
        return l.ch < r.ch;
    }
};

int main()
{
    std::set<Character, Compare> set = { {'a'}, {'B'} };

    globalCaseSensitive = false; // set ordering is unaffected => safe
}

If a weak_ptr expires, then that weak_ptr will subsequently compare differently to others due to it being null, and can no longer guarantee a strict weak ordering relation. In this case, the fix is the same: use a custom comparator which is immune to changes in shared state; owner_less is one such comparator.


How undefined is that behavior?

Undefined is undefined. There is no continuum.

If one's implementation [...] when does the behavior become undefined?

As soon as the contained elements cease to have a well-defined strict weak ordering relation.

If one's implementation [...] is the behavior still undefined? In other words, will find start to return the wrong key?

Undefined behaviour is not restricted to just returning the wrong key. It could do anything.

That sounds like [...] there is no problem because values are referenced by key not by order.

Without ordering, keys lack the intrinsic ability to reference values.

Oktalist
  • 14,336
  • 3
  • 43
  • 63
  • "The return value of the function call operation applied to an object of type _Compare_, when contextually converted to `bool`, yields `true` if **the first argument of the call appears before the second in the strict weak ordering relation** induced by this _Compare_ type, and `false` otherwise." "The following standard library facilities expect a _Compare_ type: `set`, `map`, ..." – Oktalist Apr 22 '14 at 03:19
  • I suggest you have a look in the code of your standard library implementation, to see how it uses the `Compare` template parameter. – Oktalist Apr 22 '14 at 03:28
0

std::sort requires an ordering as well. owner_less on it could be useful.

In a map or set less so -- putting a weak_ptr as the key to either is courting undefined behaviour. As you will habe to sync lifetime of the container and the pointer manually anyway, you may as well use a raw pointer (or a hand rolled non owning smart pointer that somehow handles the expiration problem) to make that clearer.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • @gracchus changing the order of elememts in a `map` or `set` without removing/reinserting makes all interaction with the container undefines behaviour. Basically the functions that walk a `map` or `set` are allowed to assume the elememts are properly ordered, and to crash/loop forever if this fails to be the case. – Yakk - Adam Nevraumont Apr 22 '14 at 02:32
  • @gracchus 24.2.4/3 last sentence in the standard linked from isocpp -- I am on phone, copy/paste/transcription difficult. You must never change the order of keys in an associative container. – Yakk - Adam Nevraumont Apr 22 '14 at 03:20