3479. semiregular-box mishandles self-assignment

Section: 25.7.3 [range.move.wrap] Status: Resolved Submitter: Casey Carter Opened: 2020-08-25 Last modified: 2023-02-07

Priority: 3

View all other issues in [range.move.wrap].

View all issues with Resolved status.

Discussion:

The exposition-only wrapper type semiregular-box — specified in [range.semi.wrap] — layers behaviors onto std::optional so semiregular-box<T> is semiregular even when T is only copy constructible. It provides copy and move assignment operators when optional<T>'s are deleted:

  1. (1.1) — […]

  2. (1.2) — […]

  3. (1.3) — If assignable_from<T&, const T&> is not modeled, the copy assignment operator is equivalent to:

    semiregular-box& operator=(const semiregular-box& that)
      noexcept(is_nothrow_copy_constructible_v<T>)
    {
      if (that) emplace(*that);
      else reset();
      return *this;
    }
    
  4. (1.4) — If assignable_from<T&, T> is not modeled, the move assignment operator is equivalent to:

    semiregular-box& operator=(semiregular-box&& that)
      noexcept(is_nothrow_move_constructible_v<T>)
    {
      if (that) emplace(std::move(*that));
      else reset();
      return *this;
    }
    

How do these assignment operators handle self-assignment? When *this is empty, that will test as false and reset() has no effect, so the result state of the object is the same. No problems so far. When *this isn't empty, that will test as true, and we evaluate optional::emplace(**this) (resp. optional::emplace(std::move(**this))). This outcome is not as pretty: emplace is specified in 22.5.3.4 [optional.assign]/30: "Effects: Calls *this = nullopt. Then initializes the contained value as if direct-non-list-initializing an object of type T with the arguments std::forward<Args>(args)...." When the sole argument is an lvalue (resp. xvalue) of type T that denotes the optional's stored value, emplace will destroy that stored value and then try to copy/move construct a new object at the same address from the dead object that used to live there resulting in undefined behavior. Mandatory undefined behavior does not meet the semantic requirements for the copyable or movable concepts, we should do better.

[2020-09-13; Reflector prioritization]

Set priority to 3 during reflector discussions.

Previous resolution [SUPERSEDED]:

This wording is relative to N4861.

  1. Modify [range.semi.wrap] as indicated:

    -1- Many types in this subclause are specified in terms of an exposition-only class template semiregular-box. semiregular-box<T> behaves exactly like optional<T> with the following differences:

    1. (1.1) — […]

    2. (1.2) — […]

    3. (1.3) — If assignable_from<T&, const T&> is not modeled, the copy assignment operator is equivalent to:

      semiregular-box& operator=(const semiregular-box& that)
        noexcept(is_nothrow_copy_constructible_v<T>)
      {
        if (this != addressof(that)) {
          if (that) emplace(*that);
          else reset();
        }
        return *this;
      }
      
    4. (1.4) — If assignable_from<T&, T> is not modeled, the move assignment operator is equivalent to:

      semiregular-box& operator=(semiregular-box&& that)
        noexcept(is_nothrow_move_constructible_v<T>)
      {
        reset();
        if (that) emplace(std::move(*that));
        else reset();
        return *this;
      }
      

[2021-06-13 Resolved by the adoption of P2325R3 at the June 2021 plenary. Status changed: New → Resolved.]

Proposed resolution: