shared_ptr alternative with no dependencies
You didn't post a complete header file (no include-guard/#pragma once), so it's unclear whether you also omitted a namespace Taylor { somewhere in there. But you should definitely use a namespace around a name as common as ref.



  virtual ~refcount() { }


Nit: You could use = default; instead of { } here, and you might get slightly better codegen on some compilers. (Virtual destructors are never actually "trivial," unfortunately, but getting the compiler to write this quasi-trivial code for you can't hurt.)



I notice that refcount-&gt;rc is public, not private. Was that a design decision?

It occurs to me that the proper name for what you're calling refcount is actually refcounted. The refcount, physically, is the data member rc. A type which derives from your base class is a refcounted type.



ref(T* obj) : obj(obj) {
  if(obj) {
    obj-&gt;rc++;
  }
}


I would write this as

explicit ref(T *obj) : obj_(obj) {
    if (obj_ != nullptr) {
        obj_-&gt;rc += 1;
    }
}


Notice the whitespace edits, the use of some kind of sigil for data members (to avoid having two different variables named obj in scope), my preference for += 1 when the increment is a stand-alone statement, the explicit comparison against nullptr in place of contextual-conversion-to-bool, and the addition of explicit. Non-explicit constructors permit implicit conversions, e.g.

struct Widget : public refcount { int data = 42; };

void print_data_of(ref&lt;Widget&gt; x) { std::cout &lt;&lt; x-&gt;data &lt;&lt; "\n"; }

int main() {
    Widget *p = new Widget;
    print_data_of(p);
    delete p;  // OOPS! Double delete!
}


It would be better (IMO of course) if this code did not compile.



template&lt;class T2&gt;
ref(const ref&lt;T2&gt;&amp; r) {
  if(r.get()) {
    obj = r.get();
    obj-&gt;rc++;
  }
}


This is extremely sketchy. Consider:

struct Widget : public refcount { int data = 42; };
struct Gadget : public refcount { int x = -1; int data = 42; };
int main() {
    ref&lt;Widget&gt; w = new Widget;  // OK...
    ref&lt;Gadget&gt; v = w;           // ...sketchy...
    std::cout &lt;&lt; v-&gt;data &lt;&lt; "\n";  // OOPS! Prints "-1", not "42"
}


This constructor should be either completely removed, or else constrained (using enable_if, C++2a requires, or some other trickery) so that it participates in overload resolution only when T2* would be convertible to T*.

One easy way to mostly-fix this would be to simply add an assertion:

template&lt;class T2&gt;
ref(const ref&lt;T2&gt;&amp; r) {
    static_assert(std::is_convertible_v&lt;T2*, T*&gt;);
    if (r != nullptr) {
        obj_ = r.get();
        obj_-&gt;rc += 1;
    }
}


Here we're lying to the library (e.g. std::is_constructible_v&lt;ref&lt;Widget&gt;, ref&lt;Gadget&gt;&gt; will still be true), but at least we prevent the client programmer from accidentally writing a program like the test case above.



Another way to fix the issue would be to rely on pointer-assignment to do the check for us. Instead of refcount *obj_;, let's make our data member look like T *obj_;. Then we can write

template&lt;class T2&gt;
ref(const ref&lt;T2&gt;&amp; r) {
    if (r != nullptr) {
        obj_ = r.obj_;  //HERE
        obj_-&gt;rc += 1;
    }
}


And then if T2* isn't convertible to T*, we'll get an error on the line marked //HERE.

Incidentally, this also solves your problem with ref&lt;T&gt;::get() and incomplete types.



T&amp; operator*() { return *get(); }
T&amp; operator*() const { return *get(); }


You don't need both versions of the function, since they do exactly the same thing. Just write the const version. (Dereferencing a pointer doesn't need to modify the pointer, remember. Const is a contract.)

T* operator-&gt;() { return static_cast&lt;T*&gt;(obj); }
const T* operator-&gt;() const { return static_cast&lt;const T*&gt;(obj); }


And in this case you've got the two versions doing different things, but that's still wrong, because they shouldn't be doing different things! Dereferencing a pointer doesn't need to modify the pointer. What you meant in both cases was simply

T* get() const { return static_cast&lt;T*&gt;(obj); }
T* operator-&gt;() const { return get(); }
T&amp; operator*() const { return *get(); }




Your void swap(ref&amp; p) should be noexcept — just like your move-constructor, which I guess you didn't write. (You should do some move semantics here!)

I recommend implementing your swap as a one-liner: std::swap(obj_, rhs.obj_);.

A member swap function will not be picked up by any standard library algorithms. If you want your swap to be actually used, you'll need to provide an ADL swap, like this:

friend void swap(ref&amp; a, ref&amp; b) noexcept { a.swap(b); }




Your queued_delete is interesting. It's misnamed, in that its deletions are stacked (LIFO), not queued (FIFO). I don't know if that makes a difference to performance or anything like that, in practice.

It's also thread-unsafe, which is not clear from your description/documentation. In standard C++, we can write

int main() {
    std::shared_ptr&lt;Widget&gt; p(new Widget);
    std::thread([q = p]() {
        q = nullptr;
    }).detach();
    p = nullptr;
}


and be guaranteed that the writes to the refcount shared by p and q won't race with each other. Your documentation clearly states that we can't do that with your ref&lt;Widget&gt;. But what is surprising to me is that we also can't do the following!

int main() {
    ref&lt;Widget&gt; p(new Widget);
    std::thread([]() {
        ref&lt;Gadget&gt; q(new Gadget);
        q = nullptr;
    }).detach();
    p = nullptr;
}


Here, p.obj-&gt;rc and q.obj-&gt;rc are completely different objects, so there's no race there; but then they each call into queued_delete and try to write to stack, and those writes race with each other. So your ref is completely unsafe for use within a multi-threaded environment, even if you never share any objects between threads.

In theory I guess you could "fix" this by replacing the storage class static with thread_local everywhere it appears; but, please don't do that.