shared_ptr basic implementation for non array types
It might be difficult to get great feedback on this shared_ptr just because it omits so many of the features of std::shared_ptr. I mean I'd be tempted to just list all the features it doesn't have and then fail to review the code at all:


Atomic/thread-safe accesses to the reference count.
Implicit conversion from shared_ptr&lt;T&gt; to shared_ptr&lt;U&gt; whenever T* is convertible to U*.
shared_ptr&lt;void&gt;.
Custom and type-erased deleters.
The "aliasing constructor."
weak_ptr.
Implicit conversion from unique_ptr.
make_shared.




~shared_ptr() noexcept {             // destructor


The comment is redundant. And so is the noexcept: in C++11-and-later, destructors are implicitly noexcept, and it's idiomatic not to write it explicitly.

Contrariwise, it is extremely important to provide a noexcept swap function! You're missing the noexcept keyword here:

void swap (shared_ptr&amp; other) {


By the way, it's weird to put a space between the name of the function and its opening parenthesis. Prefer swap(shared_ptr&amp; other).



You've got a typo in a comment: assignent. Look for typos elsewhere. Where there's one mistake, there's often more than one. (Also, eliminate that redundant comment.)



      --(*ref_count_);
        if (*ref_count_ == 0) {


Your accesses to *ref_count already aren't thread-safe; but FYI, here's where the race condition would sneak in if you just went and made ref_count a pointer to a std::atomic&lt;int&gt;. In a multithreaded environment, it might well be that *ref_count_ == 0, but that doesn't mean that you're the one whose decrement took it to 0.



constexpr shared_ptr() noexcept : ptr_{nullptr}, ref_count_{nullptr} {}


If you used C++11 non-static data member initializers (NSDMIs) for ptr_ and ref_count_, you could =default this constructor.

T *ptr_ = nullptr;
int *ref_count_ = nullptr;

constexpr shared_ptr() noexcept = default;




shared_ptr&amp; operator = (shared_ptr&amp;&amp; other) noexcept {   // move assignment operator
    *this = other;
}


Why did you bother implementing this overload at all, if you're not going to make it more efficient? Either omit it entirely (so that there's only one assignment operator), or implement it as e.g.

shared_ptr&amp; operator=(shared_ptr&amp;&amp; other) noexcept {
    if (this != &amp;other) {
        decrement_ref_count_and_delete_if_needed();
        ptr_ = std::exchange(other.ptr_, nullptr);
        ref_count_ = std::exchange(other.ref_count_, nullptr);
    }
    return *this;
}


By the way, once you make your swap function noexcept, you might consider saving some brain cells by using the copy-and-swap idiom:

shared_ptr&amp; operator=(shared_ptr&amp;&amp; other) noexcept {
    shared_ptr(std::move(other)).swap(*this);
    return *this;
}




  void reset() noexcept {
    decrement_ref_count_and_delete_if_needed();
  }


This is wrong. Consider:

shared_ptr&lt;int&gt; p(new int);
shared_ptr&lt;int&gt; q = p;
p.reset();
assert(p.get() != nullptr);  // oops




  void reset(T* ptr) {
    decrement_ref_count_and_delete_if_needed();
    ptr_ = ptr;
    if (!ref_count_) {
      ref_count_ = new int{1};
    }
    *ref_count_ = 1;
  }


This is also wrong. Consider:

shared_ptr&lt;int&gt; p(new int);  // A
shared_ptr&lt;int&gt; q = p;       // B
p.reset(new int);            // C
p = shared_ptr&lt;int&gt;();       // D
assert(q.use_count() == 0);  // oops!


Line A sets the refcount to 1. Line B increments the refcount to 2. Line C repoints p.ptr_ (but does not change p.ref_count_ — oops!), and then resets the refcount to 1. Line D decrements the refcount to 0 and frees the int that was allocated on line C. Now q.ptr_ still points to the int that was allocated on line A, but *q.ref_count_ == 0.

The correct implementation of reset(T*) is simply

  void reset(T *ptr) {
      *this = shared_ptr(ptr);
  }


(I strongly advise against ever making calls to reset from user code, by the way, because every call to reset can be more readably expressed as a simple and type-safe assignment.)



int use_count() const noexcept {
    return *ref_count_;
}


There's an unspoken precondition here: that use_count() shall never be called on a shared_ptr in the default-constructed ("disengaged", "moved-from", "partially formed") state. If you keep this precondition, then the Lakos Rule would suggest that this function shouldn't be noexcept. However, I think it would be more natural to write

int use_count() const noexcept {
    return ref_count_ ? *ref_count_ : 0;
}


so that the function always has well-defined behavior.