Reputation: 72271
Consider this program:
#include <memory>
#include <iostream>
class X
: public std::enable_shared_from_this<X>
{
public:
struct Cleanup1 { void operator()(X*) const; };
struct Cleanup2 { void operator()(X*) const; };
std::shared_ptr<X> lock1();
std::shared_ptr<X> lock2();
};
std::shared_ptr<X> X::lock1()
{
std::cout << "Resource 1 locked" << std::endl;
return std::shared_ptr<X>(this, Cleanup1());
}
std::shared_ptr<X> X::lock2()
{
std::cout << "Resource 2 locked" << std::endl;
return std::shared_ptr<X>(this, Cleanup2());
}
void X::Cleanup1::operator()(X*) const
{
std::cout << "Resource 1 unlocked" << std::endl;
}
void X::Cleanup2::operator()(X*) const
{
std::cout << "Resource 2 unlocked" << std::endl;
}
int main()
{
std::cout << std::boolalpha;
X x;
std::shared_ptr<X> p1 = x.lock1();
{
std::shared_ptr<X> p2 = x.lock2();
}
}
I don't see anything in the C++11 Standard section 20.7.2 suggesting any of this is invalid. It's a bit unusual to have two shared_ptr
objects store the same pointer &x
but not share ownership, and to use "deleters" that do not end the lifetime of *get()
, but nothing forbids it. (And if either of those are entirely unintended, it would be difficult to explain why some shared_ptr
member functions accept a std::nullptr_t
value.) And as expected, the program outputs:
Resource 1 locked
Resource 2 locked
Resource 2 unlocked
Resource 1 unlocked
But now if I add a bit to main()
:
int main()
{
std::cout << std::boolalpha;
X x;
std::shared_ptr<X> p1 = x.lock1();
bool test1( x.shared_from_this() );
std::cout << "x.shared_from_this() not empty: " << test1 << std::endl;
{
std::shared_ptr<X> p2 = x.lock2();
}
try {
bool test2( x.shared_from_this() );
std::cout << "x.shared_from_this() not empty: " << test2 << std::endl;
} catch (std::exception& e) {
std::cout << "caught: " << e.what() << std::endl;
}
}
then things get trickier. With g++ 4.6.3, I get the output:
Resource 1 locked
x.shared_from_this() not empty: true
Resource 2 locked
Resource 2 unlocked
caught: std::bad_weak_ptr
Resource 1 unlocked
Why would the second call to shared_from_this()
fail? All the requirements of 20.7.2.4p7 are met:
Requires:
enable_shared_from_this<T>
shall be an accessible base class ofT
.*this
shall be a subobject of an objectt
of typeT
. There shall be at least oneshared_ptr
instancep
that owns&t
.
[T
is X
, t
is x
, p
is p1
.]
But g++'s enable_shared_from_this
essentially follows the suggested implementation from the (non-normative) "Note" in 20.7.2.4p10, using a private weak_ptr
member in class enable_shared_from_this
. And it seems impossible to account for this sort of issue without doing something considerably more complicated in enable_shared_from_this
.
Is this a defect in the Standard? (If so, no comment is needed here on what the solution "should" be: add a requirement so the example program invokes Undefined Behavior, change the Note to not suggest such a simple implementation would be sufficient,....)
Upvotes: 12
Views: 6743
Reputation: 8270
X x;
std::shared_ptr<X> p1 = x.lock1();
(...sniped...)
}
Such code is breaks the semantics of "owning" "smart pointers":
This invariant is so essential, I'd argue that such practice should be rejected by code review. But there is a variant of what you suggest that satisfies the invariant:
So here we have shared owning objects that are part of different "families" of owning objects, they aren't "equivalent" as they have different:
use_count()
valuesowner_before
resultsbut they all prevent the destruction of the same object; this is done by keeping a copy of the shared_ptr
in each and every "deleter" object.
A clean replacement for std::shared_from_this
is used to have complete control over the initialization of the std::weak_ptr<T>
member.
#include <memory>
#include <iostream>
#include <cassert>
// essentially like std::shared_from_this
// unlike std::shared_from_this the initialization IS NOT implicit
// calling set_owner forces YOU to THINK about what you are doing!
template <typename T>
class my_shared_from_this
{
std::weak_ptr<T> weak;
public:
void set_owner(std::shared_ptr<T>);
std::shared_ptr<T> shared_from_this() const;
};
// shall be called exactly once
template <typename T>
void my_shared_from_this<T>::set_owner(std::shared_ptr<T> shared)
{
assert (weak.expired());
weak = shared;
}
template <typename T>
std::shared_ptr<T> my_shared_from_this<T>::shared_from_this() const
{
assert (!weak.expired());
return weak.lock();
}
class X : public my_shared_from_this<X>
{
public:
struct Cleanup1 {
std::shared_ptr<X> own;
Cleanup1 (std::shared_ptr<X> own) : own(own) {}
void operator()(X*) const;
};
struct Cleanup2 {
std::shared_ptr<X> own;
Cleanup2 (std::shared_ptr<X> own) : own(own) {}
void operator()(X*) const;
};
std::shared_ptr<X> lock1();
std::shared_ptr<X> lock2();
X();
~X();
};
// new shared owner family with shared ownership with the other ones
std::shared_ptr<X> X::lock1()
{
std::cout << "Resource 1 locked" << std::endl;
// do NOT call set_owner here!!!
return std::shared_ptr<X>(this, Cleanup1(shared_from_this()));
}
std::shared_ptr<X> X::lock2()
{
std::cout << "Resource 2 locked" << std::endl;
return std::shared_ptr<X>(this, Cleanup2(shared_from_this()));
}
void X::Cleanup1::operator()(X*) const
{
std::cout << "Resource 1 unlocked" << std::endl;
}
void X::Cleanup2::operator()(X*) const
{
std::cout << "Resource 2 unlocked" << std::endl;
}
X::X()
{
std::cout << "X()" << std::endl;
}
X::~X()
{
std::cout << "~X()" << std::endl;
}
// exposes construction and destruction of global vars
struct GlobDest {
int id;
explicit GlobDest(int id);
~GlobDest();
};
GlobDest::GlobDest(int id)
: id(id)
{
std::cout << "construction of glob_dest #" << id << std::endl;
}
GlobDest::~GlobDest() {
std::cout << "destruction of glob_dest #" << id << std::endl;
}
GlobDest glob_dest0 {0};
std::shared_ptr<X> glob;
GlobDest glob_dest1 {1};
std::shared_ptr<X> make_shared_X()
{
std::cout << "make_shared_X" << std::endl;
std::shared_ptr<X> p = std::make_shared<X>();
p->set_owner(p);
return p;
}
int test()
{
std::cout << std::boolalpha;
std::shared_ptr<X> p = make_shared_X();
static std::shared_ptr<X> stat;
{
std::shared_ptr<X> p1 = p->lock1();
stat = p1;
{
std::shared_ptr<X> p2 = p->lock2();
glob = p2;
std::cout << "exit scope of p2" << std::endl;
}
std::cout << "exit scope of p1" << std::endl;
}
std::cout << "exit scope of p" << std::endl;
}
int main()
{
test();
std::cout << "exit main" << std::endl;
}
Output:
construction of glob_dest #0
construction of glob_dest #1
make_shared_X
X()
Resource 1 locked
Resource 2 locked
exit scope of p2
exit scope of p1
exit scope of p
exit main
Resource 1 unlocked
destruction of glob_dest #1
Resource 2 unlocked
~X()
destruction of glob_dest #0
Upvotes: 0
Reputation: 72271
This issue and other related ones were clarified in C++17. Now std::enable_shared_from_this<T>
is specified as if having a single std::weak_ptr<T> weak_this;
member. For a non-array specialization of std::shared_ptr
, that member is assigned by std::shared_ptr
constructors, std::make_shared
, and std::allocate_shared
as described in [util.smartptr.shared.const]/1:
Enables
shared_from_this
withp
, for a pointerp
of typeY*
, means that ifY
has an unambiguous and accessible base class that is a specialization ofenable_shared_from_this
, thenremove_cv_t<Y>*
shall be implicitly convertible toT*
and the constructor evaluates the statement:if (p != nullptr && p->weak_this.expired()) p->weak_this = shared_ptr<remove_cv_t<Y>>(*this, const_cast<remove_cv_t<Y>*>(p));
So the correct behavior of the second main
in my OP is now that no exception will be thrown and both "not empty" checks will show true. Since at the call to lock2()
the internal weak_ptr
is already owned and therefore not expired()
, lock2()
leaves the weak_ptr
unchanged, and so the second call to shared_from_this()
returns a shared_ptr
which shared ownership with p1
.
Upvotes: 0
Reputation: 171253
I agree this is a hole in the specification, thus a defect. It's basically the same as http://open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2179 although that issue comes at it from a slightly different (and IMHO more obviously broken) angle.
I'm not sure I agree that this is a misuse of shared_ptr
, I think it's fine to do that with shared_ptrs, because unlike the code in issue 2179 you use no-op deleters. I think the problem is when you try to combine that kind of use of shared_ptr
with enable_shared_from_this
.
So my first thought was to fix it by extending the requirements of shared_from_this
:
Requires:
enable_shared_from_this<T>
shall be an accessible base class ofT
.*this
shall be a subobject of an objectt
of typeT
. There shall be at least oneshared_ptr
instancep
that owns&t
and any othershared_ptr
instances that own&t
shall share ownership withp
.
This isn't quite sufficient though, because your example meets that requirement: at the second call to shared_from_this()
there is only one owner (p1
) but you've already "corrupted" the state of the enable_shared_from_this
base class by calling lock2()
.
A smaller form of the program is:
#include <memory>
using namespace std;
int main()
{
struct X : public enable_shared_from_this<X> { };
auto xraw = new X;
shared_ptr<X> xp1(xraw); // #1
{
shared_ptr<X> xp2(xraw, [](void*) { }); // #2
}
xraw->shared_from_this(); // #3
}
All three of libstdc++, libc++ and VC++ (Dinkumware) behave the same and throw bad_weak_ptr
at #3, because at #2 they update the weak_ptr<X>
member of the base class to make it share ownership with xp2
, which goes out of scope leaving the weak_ptr<X>
in the expired state.
Interestingly boost::shared_ptr
doesn't throw, instead #2 is a no-op and #3 returns a shared_ptr
that shares ownership with xp1
. This was done in response to a bug report with almost exactly the same example as the one above.
Upvotes: 5
Reputation: 473212
Yes, there is a defect here in C++11. In allowing this:
It's a bit unusual to have two shared_ptr objects store the same pointer &x but not share ownership, and to use "deleters" that do not end the lifetime of *get(), but nothing forbids it.
This should be explicitly stated to be undefined behavior, regardless of what the "deleters" do. Sure, it may be technically not illegal to do things that way.
However, you are lying to people who use the code. The expectation of anyone who receives a shared_ptr
is that they now have ownership of the object. So long as they keep that shared_ptr
(or a copy thereof) around, the object it points to will still exists.
That is not the case with your code. So I would say that it is syntactically correct but semantically invalid.
The language for shared_from_this
is fine. It's the language for shared_ptr
that needs changing. It should state that it is undefined behavior to create two separate unique pointers that "own" the same pointer.
Upvotes: 6