Skip to content

Fix assignment operator of optional_t for non-trivial types#5633

Open
asimonov1 wants to merge 1 commit into
mainfrom
asimonov/fix_optional_t_assignment
Open

Fix assignment operator of optional_t for non-trivial types#5633
asimonov1 wants to merge 1 commit into
mainfrom
asimonov/fix_optional_t_assignment

Conversation

@asimonov1

@asimonov1 asimonov1 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

optional_t in src/common/optional.hpp is a fallback optional
implementation for builds with C++ < 17 (it aliases std::optional when C++17
is available). Its assignment operator had several defects — undefined behavior
for non-trivial T, a missing return, and no exception guarantee — detailed
below.

Problem

  • Copy/move assignment operated on a non-living object. Both overloads did
    value_.~T() and then value_ = other.value_ (i.e. T::operator=). After
    the explicit destructor — or when *this was empty and value_ was never
    constructed — value_ is not a live object, so running T::operator= on it
    is UB. (The constructors already do the right thing via placement-new.)
  • operator=(nullopt_t) never returned. It fell off the end of a
    value-returning function, which is UB.
  • No exception guarantee. The old value was destroyed and has_value_ set
    to true before the new value was constructed. If T's copy/move
    constructor threw, the object was left claiming to hold a value that was never
    constructed — no longer destructible (the destructor would run value_.~T()
    on non-constructed storage).

For a trivial/POD T these are benign, which is why the bug went unnoticed. For
a resource-owning T (e.g. dnnl::memory::desc, which holds a
std::shared_ptr) it corrupts memory / crashes.

The bug is currently latent: no call site assigns onto a non-trivial
optional_t. The riskiest instantiation is optional_t<memory::desc> in the
graph DNNL backend (src/graph/backend/dnnl/layout_id_mgr.cpp), which is
construct-and-read only today. This fix removes the landmine before an
assignment is ever added there.

Fix

  • Copy/move assignment now placement-new construct into value_ instead of
    running T::operator= on non-living storage.
  • operator=(nullopt_t) now returns *this.
  • has_value_ is set to true only after the placement-new succeeds, so on a
    throwing T copy/move constructor the optional is left validly empty. This
    gives the basic exception guarantee (not strong — the previous value is
    not preserved).
  • Dropped the unused parameter names.

Open questions

A couple of std::optional divergences I noticed but kept out of scope — both
involve currently-unused API:

  • value_or is unused and takes T&&, so it rejects lvalue arguments (unlike
    std::optional::value_or, which takes const T&). Leave it, fix the
    signature, or remove it?
  • operator bool is unused and non-explicit, unlike std::optional's. A
    non-explicit conversion allows surprising implicit bool conversions. Make it
    explicit, remove it, or leave it?

@rjoursler

Copy link
Copy Markdown
Contributor

@uxlfoundation/onednn-arch As std::optional is a native part of C++17, maintaining compatibility layers like this seems low value. Can we consider updating the minimum supported C++ version? Given that it C++17 was approved nearly 9 years ago, this doesn't seems like it would be a particularly stringent change.

@vpirogov

Copy link
Copy Markdown
Contributor

Can we consider updating the minimum supported C++ version?

Sure, we can consider it. It would require an RFC to document and discuss all the implications and a deprecation period. If there's an impact on ABI compatibility of existing packages it would require a major version change as well.

@asimonov1
asimonov1 force-pushed the asimonov/fix_optional_t_assignment branch from b0e8f1e to fd05208 Compare July 20, 2026 18:38
Copy/move assignment ran T::operator= on storage holding no live object,
which is UB for non-trivial T; use placement-new instead. Also add the
missing return in operator=(nullopt_t) and set has_value_ only after a
successful construct (basic exception guarantee).
@asimonov1
asimonov1 force-pushed the asimonov/fix_optional_t_assignment branch from fd05208 to 3133955 Compare July 20, 2026 19:04
@asimonov1

Copy link
Copy Markdown
Contributor Author

make test

@asimonov1

Copy link
Copy Markdown
Contributor Author

@asimonov1

Copy link
Copy Markdown
Contributor Author

make test
set test_scope=NIGHTLY

@asimonov1
asimonov1 marked this pull request as ready for review July 21, 2026 18:16
@asimonov1
asimonov1 requested a review from a team as a code owner July 21, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants