Question Details

No question body available.

Tags

c++ multithreading raii rule-of-three

Answers (3)

March 9, 2026 Score: 9 Rep: 15,824 Quality: High Completeness: 60%

Your class is actually broken as-is. The thread lambda captures this, but your class is still movable. If it is moved, the thread handle will be moved to the new object, but the running thread will still refer to the old object and use its vector, which has also been moved from.

You need to disable moving for this class, which means you already need to abandon the rule of zero. Alternatively, if your connection manager does not need access to the connections, the member should just be a local variable inside the thread function. The order issue then becomes moot. I'm assuming this is not the case though.

So another alternative is to allocate the connection list as a sharedptr and have the thread function capture the pointer by value, instead of capturing this. The order issue is not resolved, but it is now very obvious: the in-class initializer for the thread refers to the sharedptr member, which has its own in-class initializer, and so the order dependency is clear (and a good compiler should at least warn about violating it (Clang trunk does, GCC trunk doesn't). Also, for destruction, the order issue actually gone because the connection list is kept alive by the thread's own copy of the shared_ptr.

March 9, 2026 Score: 6 Rep: 15,362 Quality: High Completeness: 80%

What is the idiomatic C++20 approach to this problem?

The idiomatic C++ approach, no matter the edition, is RAII: the constructor acquires the resources, the destructor releases them.

As such, a start/stop approach is less idiomatic. It may be useful in some cases, when a delayed start is necessary, but introduces issues of its own:

  • What if the thread creation fails when calling start?
  • What if someone calls start on an already started object?
  • What if someone forgets to call stop?
  • What if someone calls stop on a not yet started object?
  • What if someone calls stop on an already stopped object?

All of those can be answered, but they must be answered, and different circumstances warrant different behaviors, so the answers, and their rationales, should be documented... documentation which may be out of the sync with the code, ...

RAII just works, departure from it should be reserved to exceptional circumstances.

Rely on Declaration Order

Be explicit.

For now, since the class only has a single data-member (beyond the thread) it works more or less as-is -- minus relocation issues -- but that's more by accident than by design.

Some of the data-members (such as activeconnections) are accessed on another thread (without synchronization) and likely by the class, others are not. And it's not clear because the lambda captures this which gives it access to everything in theory, including the very thread it's running on.

A better pattern, here, is to:

  1. Be explicit about this separation.
  2. Only capture what you need in the lambda.
  3. And handle synchronization...

In code:

class ConnectionManager {

private: struct Shared { std::mutex mutex; std::vector activeconnections{1, 2, 3}; };

std::sharedptr shared{std::makeshared()};

std::jthread worker{ [shared = shared](std::stoptoken st) { WorkerLoop(std::move(shared), st); } }; };

Note: for less brittle code, use an appropriately designed Mutex class, with a lock method returning a guard, ...

Reviewing the above:

  1. Now it's clear what is and is not being shared with the worker thread.
  2. Only what is shared is captured by the thread.
  3. There is a std::mutex to handle (very manually) proper synchronization.

And with regard to the order of the data-members:

  1. It's explicit that worker requires shared to be initialized, so hard to accidentally swap them... and at least Clang would warn about it.
  2. Due to the use of std::shared_ptr, there is no destruction order issue any longer.

The very code structure is now self-documented and less error-prone, pit of success style:

  • The path of least resistance to share one more data-member is to add it to Shared, and doing so takes care of many subtleties -- stable address, synchronization, construction/destruction order -- "by magic".
  • The path of least resistance to NOT share one more data-member is to add it to the class itself, not Shared, and doing so ensures it's not accidentally accessed by the thread.
  • The Rule of Zero is maintained, so adding one more data-member doesn't require mucking around with special members.
March 9, 2026 Score: 5 Rep: 7,969 Quality: Low Completeness: 40%

... Rely on Declaration Order: Keep the current design. It maintains the Rule of Zero, but feels fragile.

I will not question the rest of the design, but specifically to the question as asked/titled:

  • It is a fact of C++ that declaration order is a semantically relevant characteristic of a class definition.
  • Tooling offers some warnings around this, but it is "brittle" in the sense that implicit dependencies are often not caught.

I would go with clear, short comments for this, as already done in the example. Seems much better to transport in comments "what C++ maintainers should already know", than to complicate the actual code.

That being said, I personally would complicate the actual library code if it helps users of the code to make less mistakes. (Which is not the case here since we're talking about maintainers of the lib code itself.)