> it's less clear from the code that this will block forever
Sure! That doesn't imply that's a bad thing. It's not like "clarity" is transitive. Do all your callers know when you're calling lock_guard() for example? No. Should they? No.
> than it is for this:
You actually sacrifice correctness here for "clarity". What happens if you return control flow returns to the caller before join() is called, whether normally (early return, etc.) or via an exception? Suddenly you have a thread that is out of your control, and the caller has absolutely no guarantees about any aspect of it. At best, your program might terminate in the middle of that thread's work, and wreck whatever it was doing (maybe it was working on some file or communicating with another system). At worst, your code was called in a loop and you actually bring down the whole system, e.g. by launching new threads before the old ones have terminated. It's a massive bug to return with rogue threads.
> If you agree that blocking forever waiting for a thread to join is in itself something to be discouraged (not made impossible, but also not the default), then it also follows that having a destructor block at all should also be avoided in the std lib.
What I'm saying is this isn't true. Blocking should be discouraged in most cases, but this case is specifically an exception to that. Just like how "calling 'delete' directly should be discouraged" has obvious exceptions like "except in the destructor of a smart pointer". And just like how "calling mutex::unlock() directly should be discourage" has obvious exceptions like "except in the destructor of a lock guard". Your reasoning simply doesn't follow, and for good reason.
Moreover, I also beg to differ on the notion that "every blocking operation should have a timeout"; that's true in certain scenarios such as when you're waiting for a component in a distributed system (like an event on another process or machine that might go down without guaranteed notification to you), but quite false in general. Just look at lock_guard... it has no timeout, and it's used all over the place. Moreover, any operation you do that involves a heap allocation or deallocation can block indefinitely with no timeout, and it's perfectly fine. And if you use Windows, look for example at how GetMessage() has no timeout, and note that it's used in pretty much every GUI program for messaging within threads, between threads, and between processes. I could cite more examples but hopefully this illustrates what I mean.
The sense I'm getting from your arguments is that you simply don't believe and/or trust in 'scoped' operations. I don't know how to convince you here, but if you scope your operations match destructors to constructors like they're supposed to be, some things that could go wrong simply won't go wrong. Because, more abstractly, this lets you decompose your program into a tree instead of a DAG, and all the niceness that comes with that just comes automatically.