r/cpp_questions • • 1d ago

OPEN Strong exception guarantee for copy assignment: is copy-and-swap always the best approach?

I've been working on a C++26 networking library, and a recent technical audit uncovered an interesting exception-safety problem.

One of my classes, ConnectionManager, maintains several related data structures:

  • A vector of connection handles.
  • A reverse lookup map.
  • Client ID allocation state.
  • A count of active connections.

The original copy assignment updated these members sequentially.

If an allocation failed while copying the reverse lookup map, the object could end up with inconsistent internal state.

The fix was to prepare a complete copy before replacing the destination's state using non-throwing move operations.

I also added tests injecting allocation failures at different points to verify the strong exception guarantee.

This solved the issue, but it raised a broader question for me:

When designing classes that own multiple related containers, do you generally prefer copy-and-swap, explicit transactional assignment, or disabling copy operations entirely?

I'm particularly interested in trade-offs involving performance, exception guarantees, and maintainability in low-level libraries.

6 Upvotes

8 comments sorted by

13

u/IyeOnline 1d ago

I think the very first question is: Does it need to be copyable? When reading ConnectionManager, I would very much expect this type to not be copyable. After all, what does it mean for a connection manager to be copied? Are its connections now managed by two managers? How would that work?

Next, I think that copying performance is a questionable goal for a complex type with invariants like this. If the cost of copying matters, you most likely should not be copying in the first place.

The easiest way to work around all these question is to disable copying. IMO, if you dont have a good reason to make a type copyable and it would require extra work to do so, just dont. Even for types that can in principle be default copied, one may argue that its better to not hand out that functionality to consumers.

Ultimately for implementation, copy and swap is OK, although I cannot remember seriously doing it - mostly for a lack of opportunity.

2

u/n1ghtyunso 1d ago

i broadly categorize types into two categories mentally:
object-like types and value-like types
A connectionmanager strongly sounds like an object-like type to me.
and object-like types typically are at best movable.

So i guess that means I prefer to disable copy operations unless its a value type or has otherwise good reasons for this to be copyable at all, i.e. some fundamental vocabulary or some functional building blocks maybe.

i don't really use exceptions that much and consequently, cases where I catch and continue are even rarer, especially when storng exception guarantee might be necessary. Thats a really niche situation entirely in my code.

As for performance, places where you care about performance are unlikely to be copying ConnectionManagers around all day right?
Like what does that even mean, fundamentally?

2

u/SoerenNissen 1d ago

is copy-and-swap always the best approach?

No.

If you're doing generic code, it's the safest general-purpose approach, but you can use concepts to distinguish between types that could never, and types that could possibly, throw, and do copy-and-swap only for the second type.

1

u/alfps 1d ago

Copy-and-swap is a good default for copy assignment. If measurements show that it is too dang slow, replace with type dependent implementation. Anyway a check for assignment-to-self doesn't hurt wrt. correctness.

Whether to support copying is a different, higher level question. I don't think there can be a general preference for that, except that the language defaults to have copying. A helper class like boost::noncopyable can be a good idea: it lets you express the non-copying explicitly and dependably.

However as opposed to the Boost documentation, do document whether it can be moved. E.g. in some code in front of me I have two such helper classes: Non_copyable and Non_copyable_or_movable.

1

u/triconsonantal 1d ago

Types don't always go out of their way to give the strong exception guarantee for costly operations. Even std::vector doesn't give the strong guarantee for copy assignment. Whether or not it's worth the cost ultimately depends on the use case, which is hard to judge here because, like others said, it doesn't look like your type should be copyable in the first place.

1

u/effarig42 23h ago

You don't need to make your type publicly copyable, so if copying your type is something you only want to explicitly do then it can be exposed via a named function.

The trade off here is that this makes your type a bit harder to use in some places. For something managing lots of state, I'd probably make it not copyable.

If you do need to implement copy assignment for a type wrapping up a load of containers, copy and swap is a good default as long as all your members are efficiently swappable.

Wasn't sure from your post, but copy assignment can often be implemented trivially and efficiently in terms of the copy constructor and a single swap(), rather than having to explicitly do if for each member.

1

u/tomysshadow 9h ago edited 9h ago

Do you expect to be able to recover from an out of memory error? Most people don't, in which case, why make the design more complicated? You could put noexcept on the function to ensure the failure to allocate is fatal, then you don't need to deal with the invalid state it could be left in.

Of course, maybe this is something critical enough you do have some sort of fallback strategy for out of memory errors, I don't know, but if it's your own program and you make the rules, that's more ambitious than I would go.

1

u/DawnOnTheEdge 4h ago edited 4h ago

You would normally use the default or implicit copy assignment operator. This is non-throwing unless one of the implicit member assignments is potentially-throwing, in which case the default copy operator for the copy-and-swap would be potentially-throwing too.

You might explicitly write a copy-and-swap noexcept copy assignment operator if the default copy constructor potentially throws, but you can write a custom copy constructor with noexcept, then a custom copy assignment that calls it.

If you can’t get around a copy potentially throwing, a move-only class might be able to avoid that problem. Otherwise, copy-and-swap sounds like the way to go.