0

I have a struct, A, objects of which are managed by shared_ptrs. Struct A holds a reference to struct B. B objects need to keep track of which A objects hold references to them, and also need to be able to return shared_ptrs to these objects. To make this easy, I'm storing a set of weak_ptrs to associated A objects inside of B. So far, so good.

My problem is that I want A's destructor to remove the reference to itself from its associcated B object. However, (what I thought was) the obvious solution doesn't work, as by the time A's destructor is called, its associated weak_ptrs are expired, making it difficult to delete from the set. Here's what I tried:

#include <memory>
#include <set>

struct A;
struct B;

struct B{
   std::set<std::weak_ptr<A>, std::owner_less<std::weak_ptr<A>>> set_of_a;
};

struct A : std::enable_shared_from_this<A>{
   B &b;
   A(B &b):b(b){};
   ~A(){
      // bad_weak_ptr exception here
      b.set_of_a.erase(shared_from_this());
   }
};


int main(){
   B b;
   std::shared_ptr<A> a1 = std::make_shared<A>(b);
   b.set_of_a.insert(a1);
   {
      std::shared_ptr<A> a2 = std::make_shared<A>(b);
      b.set_of_a.insert(a2);
   }
   return 0;
}

What is the correct way to accomplish this? I could just have A's destructor run through B's set and erase any expired weak_ptrs, but that doesn't seem clean. I could also just convert B's set to raw A pointers, and use those raw pointers to access A's shared_from_this() when needed, but I can't help but think I'm just doing something wrong.

Kevin
  • 510
  • 4
  • 16
  • Depending on one's paradigm of ownership, I'm in the camp that objects ought not know that they are owned by shared pointers. So I wouldn't have them `std::enable_shared_from_this` because then they are self-aware they are `shared_ptr` owned. (I may be flexible if they only have static factory functions to create them, and no public constructors otherwise.) – Eljay Jul 31 '18 at 21:31
  • "I could just have A's destructor run through B's set and erase any expired weak_ptrs" isn't a bad approach, but it will be a lot cleaner if the code doing that is in a member function of B that A's destructor calls. – Ben Voigt Jul 31 '18 at 21:37
  • 2
    @Eljay Yeah, I agree `std::enable_shared_from_this` is abstraction-breaking; unfortunately I feel that way pretty often when dealing with smart pointers. – Kevin Jul 31 '18 at 21:59
  • 1
    `std::set` requires that its elements don't change sort ordering while in the container. And a `std::weak_ptr` can change its correct sort ordering as a side effect of destroying a related `std::shared_ptr`... – aschepler Jul 31 '18 at 22:05
  • 1
    I don't understand the question - the whole point of `weak_ptr` is that you *don't* need to worry about them when the pointed-to object is destroyed. – Mark Ransom Jul 31 '18 at 22:07
  • @aschepler i thought it tracked the control block, not the owned object, and thus would not change? – Caleth Jul 31 '18 at 22:09
  • @Caleth Seems you're right. But the Standard isn't very clear about it. – aschepler Jul 31 '18 at 22:55
  • @Caleth When the weak pointer is seen expired, what prevents an implementation from decreasing the ref count and eventually releasing the control block? – curiousguy Aug 01 '18 at 22:23
  • 1
    @curiousguy the standard specifies that it doesn't release the control block until all the `weak_ptr`s are gone. The *whole point* of a `weak_ptr` is having defined behaviour for a "dangling" pointer situation – Caleth Aug 01 '18 at 22:37
  • @Caleth "_the standard specifies that it doesn't release the control block until all the weak_ptrs are gone_" Where is that described? – curiousguy Aug 04 '18 at 19:11

2 Answers2

4

Don't bother eagerly erasing elements from B::set_of_a. When you lock the weak_ptr to access the object, check if it is null, and erase then.

You can't shared_from_this after ~A has started, the A has ceased to be.

Caleth
  • 52,200
  • 2
  • 44
  • 75
1

Since you haven't mentioned the compiler - if you are on a new enough compiler, then you could use weak_from_this (available from C++17):

b.set_of_a.erase(weak_from_this());

This would actually achieve what you want in a clean way since then you would just be comparing the actual weak_ptr instances instead of trying to create a new shared instance in the dtor, which logically fails now. Seems to work on coliru, but did not work on VS2017 (15.4.1) with C++17 enabled.

Update for curiousguy

This snippet:

#include <memory>
#include <set>
#include <iostream>

struct A;
struct B;

struct B{
   std::set<std::weak_ptr<A>, std::owner_less<std::weak_ptr<A>>> set_of_a;
};

struct A : std::enable_shared_from_this<A>{
   B &b;
   A(B &b):b(b){};
   ~A(){
      b.set_of_a.erase(weak_from_this());
      std::cout << "Size of set_of_a: " << b.set_of_a.size() << "\n";
   }
};


int main(){
   B b;
   std::shared_ptr<A> a1 = std::make_shared<A>(b);
   b.set_of_a.insert(a1);
   {
      std::shared_ptr<A> a2 = std::make_shared<A>(b);
      b.set_of_a.insert(a2);
   }
   return 0;
}

gives output:

Size of set_of_a: 1
Size of set_of_a: 0

on coliru (gcc 8).

Rudolfs Bundulis
  • 11,636
  • 6
  • 33
  • 71
  • Unfortunately I'm stuck on -std=c++11, however it looks like I can do the same thing by giving each A object a weak_ptr to itself after the shared_ptr is created. It isn't pretty but seems to work. – Kevin Jul 31 '18 at 21:56
  • Yeah, that is another way:) I basically just put this here in case of C++17. – Rudolfs Bundulis Jul 31 '18 at 21:57
  • @curiousguy - well the issue with OPs sample is, as mentioned in the accepted answer, that he is trying to construct a new shared instance in the dtor, which is not possible since the underlying weak_ptr is already expired. By using weak_from_this he can obtain the already expired weak_ptr from the object, and use it for lookup in the set. – Rudolfs Bundulis Aug 02 '18 at 05:13
  • @RudolfsBundulis Have you tested it? Which implementation supports that? – curiousguy Aug 02 '18 at 06:51
  • @curiousguy I tested it on coliru, seems that coliru is using gcc 8 now, so at least that works for sure:) I don't know about gcc 7, I don't have a setup to test on. – Rudolfs Bundulis Aug 02 '18 at 08:36