C++ implementation of recursive_shared_mutex
I strongly recommend not inheriting from standard types, and not using private inheritance even with your own types. If a recursive_shared_mutex IS-A shared_mutex, then announce it publicly; if it IS-NOT-A shared_mutex, then don't use inheritance at all!
(Godbolt. D_view here is isomorphic to std::reference_wrapper&lt;D&gt;, for what that's worth.)

std::size_t writer_cnt_;

should be
std::size_t writer_cnt_ = 0;

so that you don't have to remember to initialize it in the member-initializer-list of the constructor. Once you do that, you can =default your constructor. Even if you don't =default the constructor, please don't do mtx_(), cv_(), and so on. Let default constructors do their jobs; and don't force me to read more code than I need to read.

    // No writer is holding the shared_mutex.
    // Call unlock() anyway. UB expected.

This comment is just completely bogus. If this situation is &quot;should never happen, UB expected,&quot; then assert false already! Don't just go on and do some random operation like shared_mutex::unlock() when you know you're in a bad state. Assert false, and let the programmer detect and fix their bug.
Again, this follows the mantra &quot;Don't force me to read more code than I have to.&quot; Don't make me read a codepath that does shared_mutex::unlock() and returns, when really that codepath is unreachable. In fact, don't even write that if test! Just do this:
void recursive_shared_mutex::unlock() {
    auto lk = std::unique_lock&lt;std::mutex&gt;(mtx_);
    assert(writer_id_.has_value());
    assert(*writer_id_ == std::this_thread::id());
    assert(writer_cnt_ &gt; 0);
    if (--writer_cnt_ == 0) {
        std::shared_mutex::unlock();  // oops, TODO FIXME
        writer_id_ = std::nullopt;
        lk.unlock();
        cv_.notify_one();
    }
}

Notice my preferred use of lk for lock-guard variables.
Notice I'm avoiding the use of ad-hoc named methods like x.reset() in favor of value-semantic operations like x = nullopt.
You've still got a bug here, which you code-commented — but commenting a bug doesn't make it go away! Frankly, this code is literally off-topic here, because it doesn't work and you know it doesn't work.
Figure out how to use a semaphore (from the &lt;semaphore&gt; header in C++20) or an atomic, instead of the shared_mutex which you already know gives you UB.

I'm 90% sure that there's a deadlock possible between your exclusive lock() and unlock() functions. Suppose Thread A has the mutex exclusive-locked, and Thread B goes to lock it. Then Thread B will enter lock(), take an exclusive lock on mtx_, and block in std::shared_mutex::lock() because Thread B still has it locked. (Assume Thread B drops its lock just long enough to fool the cv_.wait() loop.) Finally, Thread A enters unlock() and blocks in ulock(mtx_) because Thread B still has mtx_ locked.