Regard classes with start and stop methods with suspicion

Init method in a sheep’s clothing

1
2
3
4
5
6
7
8
9
10
class some_class {
public:
  void start() {
    // ...
  }

  void stop() {
    // ...
  }
};

Innocent classes with start and stop methods are looking for trouble. They are just aliases for classes with init and cleanup methods. Or they came with different names (setup and teardown are popular with test frameworks)

They raise questions like:

  • how does the user of the class ensure stop is always called?
  • how do the constructor and start divide the responsibility to initialize?
  • if start fails (e.g. throws), is then stop also called?
  • what happens if stop throws?
  • how do the destructor and stop divide the responsibility to cleanup?

Some people think they have obvious answers to the above questions, but the more you dig, the less palatable the outcomes.

To pick just the first question, here is a potential (bad) answer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void some_fn() {
  some_class a;
  bool need_to_stop = true;
  try
  {
    a.start();
    // ...
    need_to_stop = true;
    a.stop();
  }
  catch(...)
  {
    if (need_to_stop) {
      a.stop();
    }
  }
}

Compare that with using constructor/destructor (even with a complex initialization):

1
2
3
4
void some_fn() {
  some_class a = complex_initialization_fn();
  // ...
}

So what’s the solution?

Don’t use classes with start/stop methods.

But I have to use a start method

Say you’re dealing with legacy code e.g. using a test framework with setup and teardown. Then the solution of containment might work. See here for how to bridge to using the standard C++ lifetime workflow.

But I really have to use a start method

For example to fix this threading bug. There we have base class periodic_thread that might call a method of the derived class on_fire, from a different thread, before the derived class some_periodic_thread virtual table was initialized.

The naive solution is to defer starting the thread in a start method for the periodic_thread class. In addition to the problems mentioned above, this approach spreads. The derived class some_periodic_thread needs to have a start method, and it’s usage in a function is more complex.

Whenever you use the class you need additional work whenever it is used, either directly in a function, or when aggregated either as a member or as a base class.

For an alternative, look at the std::thread class. It does not have start/stop. Instead, it receive the things it needs (e.g. the function to call), in the constructor. That means that natural usage requires that the scope of the function to call from the thread will extend the life of the std::thread instance.