This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.
std::optional<T>::transform cannot be implemented while supporting program-defined specializationsSection: 22.5.3.8 [optional.monadic] Status: New Submitter: Rasheeq Azad Opened: 2025-12-24 Last modified: 2026-01-18
Priority: Not Prioritized
View all other issues in [optional.monadic].
View all issues with New status.
Discussion:
Currently (that is, as of the draft at N5032), 22.5.3.8 [optional.monadic]
specifies that std::optional<T>::transform(F&&f)& shall do the following
(and similar for the other overloads):
Let
Mandates: […] [Note 1: There is no requirement thatUberemove_cv_t<invoke_result_t<F, decltype((val))>>.Uis movable (9.5.1 [dcl.init.general]). — end note] Returns: If*thiscontains a value, anoptional<U>object whose contained value is direct-non-list-initialized withinvoke(std::forward<F>(f), val); otherwise,optional<U>().
However, none of the standard constructors or other member functions of
optional<U> provide a surefire way to initialize the contained U value with
an expression like invoke(std::forward<F>(f), val). The closest are the
in_place_t/emplace overloads, which almost but not quite admit a generic
implementation of transform. This looks roughly like:
namespace std {
template<class _F> struct __later {
_F __f;
operator decltype(std::move(__f)())() && { return std::move(__f)(); }
};
template<class _T> class optional {
// etc.
public:
template<class _F> constexpr auto transform(_F &&__f) & {
using _U = remove_cv_t<invoke_result_t<_F, _T&>>;
if(!has_value()) return optional<_U>();
return optional<_U>(in_place, __later([&] -> _U {
return std::invoke(std::forward<_F>(__f), value());
}));
}
};
}
Unfortunately, this does not quite meet the specification. The issue is if U
is a type with a U(auto&&) constructor:
struct oops {
oops() = default;
oops(auto&&) { std::cout << "launching missiles\n"; }
};
int main() {
std::optional<int> oi(5);
oi.transform([](auto& i) { return oops(); });
// missiles get launched when they shouldn't
}
In this case, the rules for direct-initialization (see 9.5 [dcl.init] bullet 16.6.2)
will select the template constructor over the conversion function on the __later
specialization. [Complete example 1]
std::optional<T>::transform with a non-standard constructor on their
std::optional<T> primary template; roughly:
namespace std {
struct __optional_from_invocable_tag {
constexpr explicit __optional_from_invocable_tag() { }
};
template<typename _T>
class optional {
// etc.
public:
template<typename _F, typename _V>
constexpr optional(__optional_from_invocable_tag, _F &&__f, _V &&__v)
: __present(true)
, __val(std::invoke(std::forward<_F>(__f), std::forward<_V>(__v)))
{ }
template<class _F> constexpr auto transform(_F &&__f) & {
using _U = remove_cv_t<invoke_result_t<_F, _T&>>;
if(!has_value()) return optional<_U>();
return optional<_U>(
__optional_from_invocable_tag(),
std::forward<_F>(__f), value());
}
};
}
[Complete example 2]. Note that the missiles are not launched.
Now for the real issue: if a user program wants to specializestd::optional
for a program-defined type, it will have to explicitly rely on these details of
its standard library implementation in order to be supported by the standard
library's transform implementation. Specifically, it will have to provide a
non-standard constructor with a signature matching the library implementation's
expectations. (A portable implementation of transform itself is more-or-less
possible for a program-defined specialization by using a circumlocution like
std::optional<std::monostate>(std::in_place).transform(/* ... */).)
The root problem is that the standard interface of std::optional<U>
provides for direct-initialization of the contained U by arbitrary glvalues, but not
by an arbitrary prvalue (that is, by calling an arbitrary invocable). This
forces library implementations to invent their own non-standard interfaces for
doing so, which then makes it impossible for those implementations to support
program-defined specializations of std::optional that only meet the minimal
requirements of the standard, and do not support those non-standard interfaces.
The fact that std::optional<T>::transform makes implementing std::optional
while supporting program-defined specializations basically impossible does not
appear to be intentional. P0798R8, which introduced
std::optional<T>::transform, does not mention this side-effect of its
standardization.
There are at least two different resolutions that immediately come to mind.
Option A: Forbid program-defined std::optional<T> specializations
Taking this option would immediately solve the problem. However, in my opinion,
this would be unnecessarily restrictive. Specializing std::optional is a
useful thing to allow, as it allows replacing the common struct optional<T> {
union { T val; }; bool present; } representation with something more compact
when T has unused values/unused bits.
Option B: Add a std::optional<T> constructor taking an invocable
This option more-or-less formalizes existing practice, using a type tag to gate
the new constructor. It would be ideal to extend this idea to emplace and
then to the various in_place_t constructors and emplace functions in other
parts of the standard, but the wording presented here is restricted to fixing
this issue.
Changing std::optional<T&> doesn't seem strictly necessary,
but introducing a nonuniformity seems like a bad idea. I'm not 100% certain about
the wording for the new constructors.
Proposed resolution:
This wording is relative to N5032.
[Drafting Note: Two mutually exclusive options are prepared, depicted below by Option A and Option B, respectively.]
Option A: Forbid program-defined std::optional<T> specializations
Modify 22.5.3.1 [optional.optional.general] as indicated:
-2- A type
-?- The behavior of a program that adds a specialization forXis a valid contained type foroptional[…]. IfTis an object type,Tshall meet the Cpp17Destructible requirements (Table 35).optionalis undefined.
Option B: Add a std::optional<T> constructor taking an invocable
Modify 22.2.1 [utility.syn], header <utility> synopsis, as indicated:
[…]
namespace std {
[…]
template<size_t I>
struct in_place_index_t {
explicit in_place_index_t() = default;
};
template<size_t I> constexpr in_place_index_t<I> in_place_index{};
// construction from arbitrary initializers
struct from_continuation_t {
explicit from_continuation_t() = default;
};
inline constexpr from_continuation_t from_continuation{};
[…]
}
Modify 22.5.3.1 [optional.optional.general] as indicated:
namespace std {
template<class T>
class optional {
public:
[…]
// 22.5.3.2 [optional.ctor], constructors
constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;
[…]
template<class... Args>
constexpr explicit optional(in_place_t, Args&&...);
template<class U, class... Args>
constexpr explicit optional(in_place_t, initializer_list<U>, Args&&...);
template<class F, class... Args>
constexpr explicit optional(from_continuation_t, F&&, Args&&...);
template<class U = remove_cv_t<T>>
constexpr explicit(see below) optional(U&&);
[…]
};
[…]
}
Modify 22.5.3.2 [optional.ctor] as indicated:
template<class U, class... Args> constexpr explicit optional(in_place_t, initializer_list<U> il, Args&&... args);-18- Constraints: […]
[…] -22- Remarks: IfT's constructor selected for the initialization is a constexpr constructor, this constructor is a constexpr constructor.template<class F, class... Args> constexpr explicit optional(from_continuation_t, F&& f, Args&&... args);-?- Mandates:
-?- Effects: Direct-non-list-initializesdecltype(std::invoke(std::forward<F>(f), std::forward<Args>(args)...))isT.valwithstd::invoke(std::forward<F>(f), std::forward<Args>(args)...). -?- Postconditions:*thiscontains a value.
Modify 22.5.4.1 [optional.optional.ref.general] as indicated:
namespace std {
template<class T>
class optional<T&> {
public:
[…]
// 22.5.4.2 [optional.ref.ctor], constructors
constexpr optional() noexcept = default;
constexpr optional(nullopt_t) noexcept : optional() {}
[…]
template<class Arg>
constexpr explicit optional(in_place_t, Arg&& arg);
template<class F, class... Args>
constexpr explicit optional(from_continuation_t, F&& f, Args&&... args);
template<class U>
constexpr explicit(see below) optional(U&& u) noexcept(see below);
[…]
};
[…]
}
Modify 22.5.4.2 [optional.ref.ctor] as indicated:
template<class U, class Arg> constexpr explicit optional(in_place_t, Arg&& arg);-1- Constraints: […]
-2- Effects: […] -3- Postconditions: […]template<class F, class Arg> constexpr explicit optional(from_continuation_t, F&& f, Arg&& arg);-?- Mandates:
-?- Effects: Equivalent to:decltype(std::invoke(std::forward<F>(f), std::forward<Args>(args)...))isT&.convert-ref-init-val(std::invoke(std::forward<F>(f), std::forward<Args>(args)...)). -?- Postconditions:*thiscontains a value.