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\$ 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\$ Commented Nov 30, 2022 at 9:27
  • \$\begingroup\$ All excellent points - thank you. I hadn't considered what happens when proc_main() returns. \$\endgroup\$ 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\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.