Skip to content

std::optional<T>::transform does not support user specializations #172197

@howtonotwin

Description

@howtonotwin

If a user defines their own specialization of std::optional<P> for some P, then libc++'s std::optional<T>::transform (for some other type T) will not support transform into the new std::optional<P>, even if std::optional<P> satisfies all the requirements imposed on it by the standard.

This is because libc++'s definition for std::optional<T> assumes that every other std::optional<U> is also a specialization of the libc++ definition. Specifically, libc++'s definition for std::optional<T> exposes a nonstandard "private" constructor (gated with a tag whose name is a reserved identifier)

enable_if_t<_IsSame<_Tag, __optional_construct_from_invoke_tag>::value, int> = 0>
_LIBCPP_HIDE_FROM_ABI constexpr explicit optional(_Tag, _Fp&& __f, _Args&&... __args)
and, in transform, expects a similar constructor to be present on the different specialization std::optional<U>
return optional<_Up>(__optional_construct_from_invoke_tag{}, std::forward<_Func>(__f), value());
libc++'s std::optional<T> should instead be programming to the standard interface of std::optional<U>.

I believe it is possible to implement transform correctly without assuming that the target std::optional specialization has any more constructors than specified in the standard (see my implementation of transform below). If that is the case, then libc++'s non-support of user-defined std::optional specializations is a bug. (If it's not actually possible for user-defined specializations of std::optional to be correct, then I suppose libc++ is in the clear for not supporting them, but then I think we'd have a standard defect...)

Example
#include <optional>

class my_bool { // a program-defined type I might want to specialize std::optional for
  char x;
public:
  explicit my_bool(bool x) : x(x) { }
  my_bool(my_bool&&) = delete;
  ~my_bool() { };

  explicit operator bool() const { return x; }
  my_bool operator!() const { return my_bool(!bool(x)); }
};

template<> class std::optional<my_bool> {
  struct absent_t { char sentinel = 2; };
  union { my_bool present; absent_t absent; }; // saved a byte 🥳!
public:
  // relevant members
  constexpr optional() noexcept : absent() { }
  constexpr optional(nullopt_t) noexcept : optional() { }
  optional(optional const&) = delete;

  template<class ...T> requires is_constructible_v<my_bool, T...>
  constexpr explicit optional(in_place_t, T &&...t) : present(std::forward<T>(t)...) { }

  template<class L, class ...T> requires is_constructible_v<my_bool, initializer_list<L>&, T...>
  constexpr explicit optional(in_place_t, initializer_list<L> l, T &&...t) : present(l, std::forward<T>(t)...) { }
  // converting constructors and other gory details omitted

  constexpr bool has_value() const noexcept {
    return absent.sentinel != absent_t().sentinel;
  }
  constexpr ~optional() { if(has_value()) present.~my_bool(); }

  template<typename F> constexpr auto transform(F &&f) &;
};

// how *I* would implement transform (other overloads similar)
template<typename F> struct later {
  F f;
  operator decltype(auto)() && { return std::move(f)(); }
};
template<typename F> constexpr auto std::optional<my_bool>::transform(F &&f) & {
  using U = remove_cvref_t<invoke_result_t<F, my_bool&>>;
  if(has_value()) {
    return std::optional<U>(in_place, later([&] -> U {
      return std::invoke(std::forward<F>(f), present);
    }));
  } else return std::optional<U>();
}

Now my std::optional<my_bool> can transform to any libc++ std::optional<T>, as well as to itself, but libc++ std::optional<T>s don't know how to construct my std::optional<my_bool>:

#include <format>
#include <iostream>
#include <string>

int main() {
  std::optional<my_bool> mb(std::in_place, true);
  // okay: mine -> mine
  std::optional<my_bool> mb2 = mb.transform(&my_bool::operator!);

  // okay: mine -> libc++
  std::optional<std::string> fmt = mb.transform([](auto const &mb) { return std::format("{}\n", bool(mb)); });
  if(fmt) std::cout << *fmt; else std::cout << "absent\n";

  // okay: mine -> libc++
  std::optional<bool> ob = mb.transform(&my_bool::operator bool);
  // ACK! libc++ -> mine
  std::optional<my_bool> mb3 = ob.transform([](bool b) { return my_bool(b); });
}

On Godbolt

Metadata

Metadata

Assignees

No one assigned

    Labels

    libc++libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions