You have encountered the one of the shortcomings of C++ RAII: Destructors can't easily report errors, and a destructors can't fail gracefully, because they have no return value and throwing exceptions is bad idea.
So if the other thread is not responding to a request to stop, what can destructor do? It can wait more or violently destroy the other thread or leave it running, none of which are very nice options. And then it can either ignore the situation, just log it, or throw an exception despite the risk of application terminating immediately, again not a very nice set of options.
So, you have at least 3 options
- The joined thread is known to be well-behaved, so you know it will terminate promptly when requested, and join in destructor is safe. The risk is, program will hang on the join if thread isn't terminating.
- You determine that the error handling options (explained above) available in destructor are sufficient, and join in destructor is ok. You have to add the error detection and handling code to destructor (for example a timeout).
- Provide a separate "close" method, which perhaps takes timeout argument, and then returns error value or throws exception on failure.
With threads, a thread failing to terminate promptly can often be considered a programming error comparable to a segfault, so you could choose to terminate the application with helpful (to the developer) diagnostic message (2nd bullet point above), or just let the program hang (1st bullet point above). But this is a bit risky, because if down the road you need to create a thread which can't terminate quickly (it is doing a blocking system call, or it must notify the other end of a network connection, which may be slow), and it's a good idea to think how to solve this before locking down a design.