1

As far as I know the only difference in the way a constructor/destructor is called depends on the instantiation order. But is there any other difference between "normal" user classes and classes in std?

Let's say I have a class called myStackOverflow and then I instantiate an object of this class. I also have an object of type std::thread in the same scope. Are there any differences in the way the constructors or destructors of these objects are called?

The main reason I am asking this is that according to the C++ standard (section 30.3.2.3) join() is not called inside a thread destructor. You can read the why here. But in this answer on stackoverflow it is mentioned to wrap the std::thread in another class and then call join() in the destructor of this new wrapper class. I don't understand how that would solve anything. A destructor is still a destructor and the dangers of calling join() in the std::thread's destructor are still there.

The only way that would make sense is that there is a difference between the way the destructor of these two different type is called.

Community
  • 1
  • 1
Ali
  • 1,001
  • 2
  • 14
  • 32
  • 3
    There's no difference – Justin Apr 05 '17 at 23:54
  • 6
    I think you're oversimplifying and misrepresenting. If you write your own code, then *you* can decide whether in your situation joining in the destructor is appropriate. But that does not mean that joining in the destructor is *always* appropriate for every user in *every* situation. In situations where auto-joining is a problem, you would of course *not* use a joining wrapper. – Kerrek SB Apr 05 '17 at 23:55
  • The answer you link doesn't actually say to call `join` in the destructor. It says to wrap the thread in another class "that will have desired behavior on destruction". If the behavior you desire is to join, and you're okay with the tradeoffs that involves, you can do that. It won't solve the problems with calling `join` in the destructor, and the answer doesn't claim it will. – user2357112 Apr 05 '17 at 23:58
  • @KerrekSB How would you even know if the situation is right for auto joining or not? If it was that simple wouldn't be already implemented in the standard? – Ali Apr 06 '17 at 00:19
  • @user2357112 Yes, it doesn't explicitly say to call `join` but I think it is implied. It says `You could always workaround this using RAII`, and as far as I understand "this" refers to calling `join`/`detach` and "workaround" usually implies a solution to the problem. – Ali Apr 06 '17 at 00:26
  • 2
    @Ali: That's a super broad question that won't get anywhere near a concise answer. You analyze your requirements and design your implementation, and you'll discover in the process how your concurrency model should look. We don't have `auto_solve_everything` in the standard library (yet). Chances are you probably won't be exposing raw `std::thread`s to the user anyway and have a more comprehensive and integrated concurrency infrastructure. – Kerrek SB Apr 06 '17 at 00:29
  • @KerrekSB Thanks for your comment. I am not really an expert in concurrency, I am a noob actually, but I feel that it's much better/safer to make sure all threads are joined properly rather than spending the time and effort to design a `sometime_joinable` class. – Ali Apr 06 '17 at 00:48
  • 1
    All class objects are known as *user defined types* including everything in the standard library. I think that one or two things in the standard library may require behind the scenes compiler magic though. But literally only one or two. – Galik Apr 06 '17 at 00:58
  • 1
    @Galic which magic? Just wandering – Gian Paolo Apr 06 '17 at 05:36

1 Answers1

0

Okay @Ali , let's take a brief overview about some things (terms).

namespace std - is a simple namespace, like:

namespace my_namespace {
    int my_integer;
};

which contains tons of useful classes for programmers, written by ultra overpowered ones, and , as you understand it should be as flexible as possible (later "Statement 1"), casue different people have different needs.

And of course it obeys the general rules of c++ standart, as it contents also do.


Now lets talk about your desired std::thread.

its a simple class that represents a single thread of execution. It allows you to execue your functions in "outerspace", and keeps in touch with our abstract "outerspace", it also handles some data:

  • _id - thread ID
  • _handle - the secret variable, which is the key for communication with "outerspace"
  • and of course it keeps some data about state of execution (does any entity executes now somewhere in the "outerspace" where the key leads us to)

if you look up some references in more details and keep in mind our "Statement 1", you will notice the following information:

  • "std::thread objects may also be in the state that does not represent any thread"
  • "No two std::thread objects may represent the same thread of execution; std::thread is not CopyConstructible or CopyAssignable, although it is MoveConstructible and MoveAssignable."

now you should come to a conclusion that std::thread variable and executing entity are separate, but trying to remove std::thread variable that is attached to an executing entity throws an exception.

but when the entity finishes execution, std::thread variable keeps alive, and may be attached to any other entity.

for theese needs there are following methods:

 join() // waits for a thread to finish its execution
 // if it is attached to something, the code execution will not go futher until our entity finishes its execution.

 detach() // permits the thread to execute independently from the thread handle 
// detaches our std:: thread variable from executing entity, now our entity lives its own life. std::thread variable may be removed while the entity keeps alive

joinable() // checks whether the thread is joinable, i.e. potentially running in parallel context
// if thread is attached to something which is executing now, it returns true, otherwise false

Here some code examples that will clear out your misunderstanding:

#include <iostream>
#include <thread>
#include <chrono>

using namespace::std;

void some_func1() {
    cout << "some_func1 thread started " << endl;
    this_thread::sleep_for(chrono::seconds(2));
    cout << "some_func1 thread finished " << endl;
}

void some_func2() {
    cout << "some_func2 thread started " << endl;
    this_thread::sleep_for(chrono::seconds(2));
    cout << "some_func2 thread finished " << endl;
}


int main() {    

    thread some_thread;

    cout << "Is some_thread joinable: " << some_thread.joinable() << endl;

    some_thread = thread(some_func1);

    cout << "Is some_thread joinable: " << some_thread.joinable() << endl;

    some_thread.detach();

    cout << "Is some_thread joinable: " << some_thread.joinable() << endl;

    this_thread::sleep_for(chrono::seconds(1)); 

    some_thread = thread(some_func2);

    cout << "Is some_thread joinable: " << some_thread.joinable() << endl;

    some_thread.join();

    cout << "Is some_thread joinable: " << some_thread.joinable() << endl;
}

//  Output is:
//  Is some_thread joinable: 0
//  some_func1 thread started
//  Is some_thread joinable: 1
//  Is some_thread joinable: 0
//  some_func2 thread started
//  Is some_thread joinable: 1
//  some_func1 thread finished
//  some_func2 thread finished
//  Is some_thread joinable: 0
//  Press any key to continue . . .

If you want to be ensured, that your entity finishes execution before your thread variable removes you may wrap it in another class and call join() in destructor, as you mentioned in question.

or you may also wrap it in another one which will call detach() in destructor.

theese 2 ways will prevent you from huge amount of crushes.

#include <iostream>
#include <thread>
#include <chrono>

using namespace std;


typedef void Myfunc();

void some_func1() {
    cout << "some_func1 thread started " << endl;
    this_thread::sleep_for(chrono::seconds(2));
    cout << "some_func1 thread finished " << endl;
}

void some_func2() {
    cout << "some_func2 thread started " << endl;
    this_thread::sleep_for(chrono::seconds(2));
    cout << "some_func2 thread finished " << endl;
}

class ICareAboutThread {
    std::thread thread_;
public:
    ICareAboutThread( Myfunc f = nullptr) : thread_(f) {};
    ~ICareAboutThread() { join(); }
    bool joinable() { return thread_.joinable();}
    void join() { thread_.join();}
    void detach() { thread_.detach();}

    // other constructors : move , safe copying - if necessary;
};


class IDontCareAboutThread {
    std::thread thread_;
public:
    IDontCareAboutThread(Myfunc f = nullptr) : thread_(f) {};
    ~IDontCareAboutThread() { detach(); }
    bool joinable() { return thread_.joinable(); }
    void join() { thread_.join(); }
    void detach() { thread_.detach(); }

    // other constructors : move , safe copying - if necessary;
};

int main() {    

    ICareAboutThread i_care(some_func1);

    this_thread::sleep_for(chrono::seconds(1));

    IDontCareAboutThread i_dont_care(some_func2);

    return 0;
}

//  Output is:
//  some_func1 thread started
//  some_func2 thread started
//  some_func1 thread finished
//  Press any key to continue . . .

now i hope its crystal clear for you, if you understand when variable removes :D

bobra
  • 615
  • 3
  • 18