What is the point of owner_less if an expired weak_ptr will give undefined behavior?

StackOverflow https://stackoverflow.com/questions/23209009

  •  07-07-2023
  •  | 
  •  

Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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.

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top