Title
semiregular-box mishandles self-assignment
Status
resolved
Section
[range.move.wrap]
Submitter
Casey Carter

Created on 2020-08-25.00:00:00 last changed 41 months ago

Messages

Date: 2021-06-13.00:00:00

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

Date: 2020-09-15.00:00:00

[ 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;
      }
      
Date: 2020-08-25.00:00:00

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 [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.

History
Date User Action Args
2021-06-14 14:09:26adminsetstatus: new -> resolved
2020-09-13 15:00:57adminsetmessages: + msg11483
2020-08-29 12:07:13adminsetmessages: + msg11464
2020-08-25 00:00:00admincreate