开发者

C++ using this pointer in constructors

开发者 https://www.devze.com 2022-12-24 07:50 出处:网络
In C++, during a class constructor, I started a new thread with this pointer as a parameter which will be used in the thread e开发者_如何学Cxtensively (say, calling member functions). Is that a bad th

In C++, during a class constructor, I started a new thread with this pointer as a parameter which will be used in the thread e开发者_如何学Cxtensively (say, calling member functions). Is that a bad thing to do? Why and what are the consequences?

My thread start process is at the end of the constructor.


The consequence is that the thread can start and code will start executing a not yet fully initialized object. Which is bad enough in itself.

If you are considering that 'well, it will be the last sentence in the constructor, it will be just about as constructed as it gets...' think again: you might derive from that class, and the derived object will not be constructed.

The compiler may want to play with your code around and decide that it will reorder instructions and it might actually pass the this pointer before executing any other part of the code... multithreading is tricky


Main consequence is that the thread might start running (and using your pointer) before the constructor has completed, so the object may not be in a defined/usable state. Likewise, depending how the thread is stopped it might continue running after the destructor has started and so the object again may not be in a usable state.

This is especially problematic if your class is a base class, since the derived class constructor won't even start running until after your constructor exits, and the derived class destructor will have completed before yours starts. Also, virtual function calls don't do what you might think before derived classes are constructed and after they're destructed: virtual calls "ignore" classes whose part of the object doesn't exist.

Example:

struct BaseThread {
    MyThread() {
        pthread_create(thread, attr, pthread_fn, static_cast<void*>(this));
    }
    virtual ~MyThread() {
        maybe stop thread somehow, reap it;
    }
    virtual void id() { std::cout << "base\n"; }
};

struct DerivedThread : BaseThread {
    virtual void id() { std::cout << "derived\n"; }
};

void* thread_fn(void* input) {
    (static_cast<BaseThread*>(input))->id();
    return 0;
}

Now if you create a DerivedThread, it's a best a race between the thread that constructs it and the new thread, to determine which version of id() gets called. It could be that something worse can happen, you'd need to look quite closely at your threading API and compiler.

The usual way to not have to worry about this is just to give your thread class a start() function, which the user calls after constructing it.


Depends on what you do after starting the thread. If you perform initialization work after the thread has started, then it could use data that is not properly initialized.

You can reduce the risks by using a factory method that first creates an object, then starts the thread.

But I think the greatest flaw in the design is that, for me at least, a constructor that does more than "construction" seems quite confusing.


It can be potentially dangerous.

During construction of a base class any calls to virtual functions will not despatch to overrides in more derived classes that haven't yet been completely constructed; once the construction of the more derived classes change this changes.

If the thread that you kick-off calls a virtual function and it is indeterminate where this happens in relation to the completion of the construction of the class then you are likely to get unpredictable behaviour; perhaps a crash.

Without virtual functions, if the thread only uses methods and data of the parts of the class that have been constructed completely the behaviour is likely to be predictable.


I'd say that, as a general rule, you should avoid doing this. But you can certainly get away with it in many circumstances. I think there are basically two things that can go wrong:

  1. The new thread might try to access the object before the constructor finishes initializing it. You can work around this by making sure all initialization is complete before you start the thread. But what if someone inherits from your class? You have no control over what their constructor will do.
  2. What happens if your thread fails to start? There isn't really a clean way to handle errors in a constructor. You can throw an exception, but this is perilous since it means that your object's destructor will not get called. If you elect not to throw an exception, then you're stuck writing code in your various methods to check if things were initialized properly.

Generally speaking, if you have complex, error-prone initialization to perform, then it's best to do it in a method rather than the constructor.


Basically, what you need is two-phase construction: You want to start your thread only after the object is fully constructed. John Dibling answered a similar (not a duplicate) question yesterday exhaustively discussing two-phase construction. You might want to have a look at it.

Note, however, that this still leaves the problem that the thread might be started before a derived class' constructor is done. (Derived classes' constructors are called after those of their base classes.)

So in the end the safest thing is probably to manually start the thread:

class Thread { 
  public: 
    Thread();
    virtual ~Thread();
    void start();
    // ...
};

class MyThread : public Thread { 
  public:
    MyThread() : Thread() {}
    // ... 
};

void f()
{
  MyThread thrd;
  thrd.start();
  // ...
}


It's fine, as long as you can start using that pointer right away. If you require the rest of the constructor to complete initialization before the new thread can use the pointer, then you need to do some synchronization.


Some people feel you should not use the this pointer in a constructor because the object is not fully formed yet. However you can use this in the constructor (in the{body} and even in the initialization list) if you are careful.

Here is something that always works: the {body} of a constructor (or a function called from the constructor) can reliably access the data members declared in a base class and/or the data members declared in the constructor's own class. This is because all those data members are guaranteed to have been fully constructed by the time the constructor's {body} starts executing.

Here is something that never works: the {body} of a constructor (or a function called from the constructor) cannot get down to a derived class by calling a virtualmember function that is overridden in the derived class. If your goal was to get to the overridden function in the derived class, you won't get what you want. Note that you won't get to the override in the derived class independent of how you call the virtual member function: explicitly using the this pointer (e.g., this->method()), implicitly using the this pointer (e.g., method()), or even calling some other function that calls the virtual member function on your this object. The bottom line is this: even if the caller is constructing an object of a derived class, during the constructor of the base class, your object is not yet of that derived class. You have been warned.

Here is something that sometimes works: if you pass any of the data members in this object to another data member's initializer, you must make sure that the other data member has already been initialized. The good news is that you can determine whether the other data member has (or has not) been initialized using some straightforward language rules that are independent of the particular compiler you're using. The bad news is that you have to know those language rules (e.g., base class sub-objects are initialized first (look up the order if you have multiple and/or virtual inheritance!), then data members defined in the class are initialized in the order in which they appear in the class declaration). If you don't know these rules, then don't pass any data member from the this object (regardless of whether or not you explicitly use the thiskeyword) to any other data member's initializer! And if you do know the rules, please be careful.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号