Well, the other solution is to use:
// saveThread.join(); // this would have blocked the UI thread until the save is completed
saveThread.detach(); // saveThread is now fire and forget, you can not join it.
but be advised that any memory accessed in saveThread must remain in memory until it finishes and you don’t know when that will be without waiting on some object like a condition variable or a semaphore, which is what the saveThread.join() did to begin with.
Multithreaded programming is … er … tricky.
Yes, a saveThread.join() in the destructor is a good idea. But also, you need to make sure no other thread is modifying controlData and the data array it references while the saveThread is running, otherwise what you are you saving?
Furthermore, saveThread in the example code happily existed on the stack but if you make it a member variable so you can join in the destructor, you must also protect yourself against the case where the class instance is destroyed without the thread being created at all or where autoSaveTriggered is called when a thread has already been created in a previous invocation.
Saving 18 megabyte of data in a single write does not take all that long, mind you, a few tens of milliseconds. It’s not like you are writing it to old-fashioned 9-track reel tapes.