5
\$\begingroup\$

By analogy with std::thread, I've written an RAII POSIX process:

class posix_process
{
public:
    explicit posix_process(std::function<void()> proc_main)
    : _pid(fork())
    {
        if (_pid == -1)
            throw std::system_error(errno, std::generic_category(), "fork");
        if (_pid == 0)
            proc_main();
    }

    pid_t pid() const
    {
        return _pid;
    }

    int wait(int options = 0) const
    {
        int wstatus = 0;
        const pid_t r = waitpid(_pid, &wstatus, options);
        if (r == -1)
            throw std::system_error(errno, std::generic_category(), "waitpid");
        return wstatus;
    }

private:
    // Disable copy and move
    posix_process(const posix_process&) = delete;
    posix_process(posix_process&&) = delete;
    posix_process& operator=(posix_process) = delete;

    pid_t _pid;
};

My main hesitation is that the child process ends up in a potentially weird state where the entire thing runs inside a constructor that was called in the parent process.

  • Is this a good idea?
  • What are the implications of making the class swappable by swapping _pid (and therefore, with minimal extra effort, moveable)?
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Have you looked at boost::process::child? You may be able to use that, or if not, to take inspiration from it. \$\endgroup\$ Commented Nov 30, 2022 at 7:51
  • 1
    \$\begingroup\$ @TobySpeight No, I haven't - thanks for the pointer! \$\endgroup\$
    – jezza
    Commented Nov 30, 2022 at 9:40

2 Answers 2

6
\$\begingroup\$

Consider what happens after proc_main() finishes

My main hesitation is that the child process ends up in a potentially weird state where the entire thing runs inside a constructor that was called in the parent process.

The problem is not so much calling proc_main() from a constructor, the problem is what happens after proc_main() finishes: the child process will continue to run, seemingly as if it was the parent, except _pid is now 0. I think the expectation is that only proc_main() runs and then the process exits. Also consider that proc_main() might throw an exception. So it might be better to write:

if (_pid == 0) {
    try {
        proc_main();
    } catch(...) {
        std::abort();
    }
    std::exit(EXIT_SUCCESS);
}

Make it look even more like std::thread

If you want something analogous to std::thread, go further and make the interface look like std::thread as much as possible. This makes it easier for someone who already knows std::thread to use your class. For one, instead of taking a std::function<void()> as a parameter, copy what std::thread does:

template <class Function, class... Args>
explicit posix_process(Function&& f, Args&&... args): _pid(fork())
{
    if (_pid == -1)
        throw std::system_error(errno, std::generic_category(), "fork");

    if (_pid == 0)
    {
        try {
            std::invoke(std::forward<Function>(f), std::forward<Args>(args)...);
        } catch(...) {
            std::abort();
        }
        std::exit(EXIT_SUCCESS);
    }
}

And rename pid() to id() and/or native_handle().

Make it moveable

What are the implications of making the class swappable by swapping _pid (and therefore, with minimal extra effort, moveable)?

That would be very nice. This will allow std::vector<posix_process> and many other things that require the class to be at least moveable.

Add a destructor

You might want to add a destructor that does something sensible rather than just letting a potential child process continue to run. std::thread will throw an exception if the thread is joinable and hasn't been joined yet, C++20's std::jthread will automatically join in its destructor, and that is usually preferred.

On the other hand, the semantics for a thread and a process are different, and it's much less dangerous to let a child process run without ever waiting for it, so if you have a good reason for it, I would probably accept not having a destructor.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Might be a good idea to std::exit() (or perhaps even std::_Exit()) with the result of the function, if it returns something convertible to int. And perhaps the catch should std::abort(), so that this is distinguishable by WIFSIGNALED()? It's somewhat complicated by the state that's inherited across the fork(). \$\endgroup\$ Commented Nov 30, 2022 at 7:47
  • \$\begingroup\$ Good point. Although std::threads don't have a return value, and a process can only return a small integer, so I don't think it makes much sense to do std::exit(proc_main()). \$\endgroup\$
    – G. Sliepen
    Commented Nov 30, 2022 at 9:27
  • \$\begingroup\$ All excellent points - thank you. I hadn't considered what happens when proc_main() returns. \$\endgroup\$
    – jezza
    Commented Nov 30, 2022 at 9:45
5
\$\begingroup\$

Construction and copying

I don't think we need to make this class non-moveable, but if we did, it's sufficient to delete just the copy constructor and assignment operator - we won't get compiler-generated move operations:

    // Disable copy and move
    posix_process(const posix_process&) = delete;
    void operator=(const posix_process&) = delete;

(Note that we can get away with declaring void return when we delete members).

As I said, I don't see any reason the class can't be moveable, so instead I'd write

    posix_process(posix_process&&) = default;
    posix_process& operator=(posix_process&&) = default;

Again, the presence of these declarations inhibits the compiler-provided copy operations, so we don't need any =delete.

Increase the abstraction of wait()

The wait() function is an extremely thin veneer over the underlying waitpid(). In particular, the options argument is not type-safe (consider defining an enum class for these, and accepting 0 or more of them as arguments instead).

Its return value is equally opaque - a raw exit status that calling code must unpack with macros such as WIFEXITED(). Again, this could be more user-friendly - perhaps we should return a structure with this information already extracted ready for use? At a minimum, enclose the raw value in a class with member functions to access these values.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.