-
Notifications
You must be signed in to change notification settings - Fork 15.5k
Description
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)
llvm-project/libcxx/include/optional
Lines 925 to 926 in 7db9769
| 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) |
transform, expects a similar constructor to be present on the different specialization std::optional<U> llvm-project/libcxx/include/optional
Line 1144 in 7db9769
| return optional<_Up>(__optional_construct_from_invoke_tag{}, std::forward<_Func>(__f), value()); |
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