std::movable_box: Give Users the Ranges Movable Wrapper

Document #: P4346R0 [Latest] [Status]
Date: 2026-10-23
Project: Programming Language C++
Audience: LEWG
Reply-to: Steve Downey
<>

1 Abstract

The exposition-only class template movable-box is the workhorse of the range adaptors. It is how the standard library stores a predicate, a projection, or a value inside a view type and still satisfies the assignability the view concept demands. Every implementation ships one. Every ranges-adjacent library reimplements one. Anyone writing a conforming view outside the standard library must write one.

This paper proposes making the type available to users as std::movable_box, with two constraints on the proposal itself:

  1. The interface is exactly the usage surface of clause [ranges]. The type supports the operations the standard library’s views perform on movable-box today, and no more. It is a tool for implementing range types, not a new general-purpose maybe-type.

  2. The specification is standalone. [range.move.wrap] specifies movable-box as a delta against optional as a wording convenience. That convenience reads as a structural mandate, and it is not one: shipping implementations differ on whether optional is involved at all. The proposed wording is self-contained and does not mention optional.

The semantics of the ranges library are unchanged; the exposition-only name is redefined as an alias for the public type, and the changes to clause [ranges] are borderline editorial.

2 Revision History

2.1 R0

Initial revision.

3 Motivation and Scope

3.1 The problem movable-box solves

Function objects in C++ are frequently not assignable. A lambda with a capture has a deleted copy assignment operator. Types with const or reference members are not assignable. Yet the range adaptors need to store such objects inside views, and views must model movable.

movable-box<T> ([range.move.wrap]) augments a move_constructible object type with assignability: if T is copy constructible but not copyable, copy assignment is synthesized as destroy-then-copy-construct; if T is not movable, move assignment is synthesized as destroy-then-move-construct. A disengaged state exists, but it is an exception artifact (reachable only when a synthesized operation exits via an exception), not a domain state. In this respect the type is the inverse of optional: its default state is engaged (a value-initialized T), and emptiness is never requested, only suffered.

3.2 Everyone already has one

The standard library uses movable-box pervasively. At least the following are specified in terms of it: single_view, repeat_view, filter_view, transform_view, take_while_view, drop_while_view, zip_transform_view, adjacent_transform_view, and chunk_by_view.

Consequently every implementation ships an internal version: libstdc++ has std::ranges::__detail::__box, libc++ has __movable_box, and the Microsoft STL has _Movable_box. The design originates in range-v3’s semiregular_box [range-v3], which user code has been borrowing or reimplementing for the better part of a decade.

Users writing their own views and range adaptors need this component to store their callables and values. Writing them is a task the ranges design intends to support; view_interface and range_adaptor_closure were made public for it. Today their choices are:

  1. Reimplement it, including the subtle valueless-by-exception corner cases and the storage optimization.
  2. Reach into implementation detail namespaces, with the usual consequences.
  3. Depend on range-v3 or another library for one small class template.

None of these is good. The type is nearly fully specified in the working draft already; the standard is simply keeping it to itself.

3.3 Scope: exactly the standard library’s usage, no more

The target user is an implementer of new or existing range types. So the proposal standardizes the observed usage surface of movable-box in clause [ranges]. An audit gives:

That is the entire interface proposed. Everything else optional offers is absent; see the design decisions and the consolidated non-goals in Non-goals.

3.4 History

The wrapper entered the working draft with the ranges design (as exposition-only semiregular-box, [range.semi.wrap] in C++20). [P2325R3] removed the requirement that views be default constructible and renamed the wrapper copyable-box. [P2494R2] relaxed the range adaptors to admit move-only function objects, arriving at today’s movable-box. The specification has been stable since; the type has shipped, in essentially its current form, in three major implementations across two standard cycles.

4 Design Decisions

The guiding principle is: standardize the type that exists, for the use cases the standard library keeps finding for it. Where the existing specification leaves a choice, prefer the resolution already made by [range.move.wrap] [range.move.wrap] and by shipping implementations.

Three principles decide the rest of this section.

Fail loudly, not subtly. The type is proposed for the use cases it already matches. Used outside them, it must not compile. An operation is omitted when supplying it would give an answer that is defensible in isolation and wrong in context; the empty state drives most of these, because it is an exception artifact rather than a domain state, and any operation whose semantics turn on it computes over something that should not have been reachable. movable_box<T&> is ill-formed for the same reason (Constraints). This is the warrant for the interface and the non-goals.

A drop-in for what ships now. A vendor must be able to implement std::movable_box as their existing box and alias the exposition-only name to it, with no view changing layout or behavior. This is why the wording mandates no structure (Specification shape) and why nothing in the interface can make the empty state reachable on demand (Interface).

Adoption is bounded. It should be a rename plus a bounded, mechanical set of edits, after which library implementers owe this component nothing further. A delta specification fails this test on its own: it makes every future change to optional into a standing question about movable_box (Specification shape).

The drop-in principle is a condition on the paper; the other two are preferences. If adopting std::movable_box would mandate an ABI break in libstdc++, libc++, or the Microsoft STL, the proposal should be rejected, and I will withdraw it. There is no version of this component worth a break: the whole claim is that the type already exists and users cannot reach it. See Implementation Experience for what has been confirmed and what has not.

4.1 Semantics

std::movable_box<T>, restricted to the interface above, has exactly the semantics specified in [range.move.wrap]. The exposition-only movable-box is redefined as an alias for it, so clause [ranges] changes are borderline editorial and no implementation needs to change behavior; vendors may literally rename (or alias) their internal type.

4.2 Name

Proposed: movable_box.

The working draft has called this type semiregular-box, copyable-box, and movable-box as its constraints evolved; the current name accurately describes what the wrapper guarantees (the result always models movable) and matches the name users of the working draft and of cppreference already know.

Alternatives considered:

4.3 Namespace

Proposed: namespace std.

Although the specification currently lives in [ranges], nothing about the type’s semantics is ranges-specific. Placing it in std::ranges would be defensible, given the ranges-shaped scope of this proposal. However, I have a weak preference for std, matching the other small vocabulary components that view authors reach for (in_place_t, and the concepts of [concepts]), and avoiding a second-class name if other library clauses later adopt it. LEWG direction welcome.

Proposed: <utility>.

With the specification decoupled from optional (Specification shape), <optional> loses its claim. The type’s constituency includes every author of a view type, all of whom include <ranges>; but per Namespace the component is not ranges-specific. <utility> is where small, broadly applicable vocabulary components live, and the addition is tiny.

Alternatives: a new <movable_box> header (defensible under the one-component-one-header trend; I am not opposed), <optional> (rejected: implies a structural relationship this paper explicitly disclaims), <ranges> (rejected per Namespace).

4.5 Specification shape

Proposed: a self-contained specification that never references optional.

[range.move.wrap] says movable-box “behaves exactly like optional<T>” with enumerated differences. For an exposition-only type whose every use is visible to the specification’s authors, that is an economical technique. However, for a public type it is the wrong shape, twice over:

The proposed wording is therefore self-contained: it defines “contains a value” for movable_box directly, specifies each member’s effects in its own right, and never references optional. That is about a page of wording. It also means the disengaged state’s semantics are stated where they belong, as postconditions of the synthesized assignments, rather than inherited from a type whose disengaged state is a first-class feature.

4.6 Interface

Proposed: the operations enumerated in Scope: default construction, direct construction from const T&/T&&, in_place construction, the special member functions with the synthesized assignments, operator*, operator->, has_value, and operator bool.

Two things point the same way:

The engagement observers are retained even though views use them only in preconditions: the valueless-by-exception state is real, and a type that can be empty but cannot be asked is a trap. variant makes the same admission with valueless_by_exception.

The constructors from const T& and T&& are proposed as explicit and non-template. The ranges clauses use only direct-initialization from T itself, so implicit conversion and a templated converting constructor (with its attendant constraint and CTAD questions) are not needed and not proposed.

4.7 Constraints

Unchanged: move_constructible<T> && is_object_v<T>.

The constraint implies two boundaries rather than stating them. Both will be asked about, so:

4.8 Storage and triviality

The recommended practice of [range.move.wrap] p2 is carried over verbatim. Layout is unspecified generally, so it stays a quality-of-implementation matter.

Because the specification is standalone (Specification shape), the triviality guarantees that were previously inherited must now be stated: the destructor is trivial if is_trivially_destructible_v<T> is true, and the copy/move constructors are trivial if T’s corresponding constructors are trivial. These are implementable in the presence of a discriminant (as optional demonstrates) and matter in practice: a trivially copyable movable_box<T> keeps views of trivially copyable payloads trivially copyable, with the ABI and calling-convention consequences that implies. The assignment operators are also specified trivial under the conditions of [optional.assign], so trivially copyable (and trivially assignable) payloads yield trivially copyable boxes — and, transitively, trivially copyable views.

4.9 Non-goals

The following are absent. Each is additive and none is precluded for the future. Each is absent now because it would answer a question over the empty state.

4.10 constexpr

Fully constexpr, as the exposition-only type already must be for the views to be usable in constant expressions.

4.11 Feature test macro

__cpp_lib_movable_box, in <utility> and <version>.

4.12 Freestanding

The proposed interface has no throwing accessors and no dependency on bad_optional_access; nothing obstructs marking the subclause freestanding. To be confirmed during wording review.

5 Impact on the Standard

A pure library extension: one new class template, one feature test macro, and an editorial redefinition of the exposition-only movable-box as an alias for the new public type. No existing valid program changes meaning. No implementation is required to change the behavior, layout, or ABI of any ranges component.

6 Implementation Experience

The proposed interface has been checked against all three shipping implementations of movable-box. In each, what the vendor’s box lacks can be added without changing any layout. What remains is one wording decision, the self-assignment guarantee (Open Questions, item 3), and a conforming strengthening of the move assignment’s noexcept, which needs no work.

Three independent implementations of this type ship today inside libstdc++, libc++, and the Microsoft STL, exercised by the entire range adaptor test suite of each. range-v3’s semiregular_box [range-v3] is the design’s origin and has a decade of field experience.

A reference implementation of the proposed interface exists as beman.movable_box [beman.movable_box], with a test suite written against the wording below rather than against the implementation. That repository is not public yet; the cited URL is where it will be published. The same suite compiles and runs, unmodified, against libstdc++’s exposition-only std::ranges::__detail::__box. Thirty-six of its forty test cases pass there.

The four failures are not defects. [range.move.wrap] specifies the exposition-only type well, and an exposition-only type owes correct behavior only where the library uses it; every difference below is either unobservable through the views, or is not required by the current exposition-only wording. The question is what adopting the public interface asks of a vendor: whether the differences can be closed without breaking any currently conforming program, and without moving any layout.

The first two were closed experimentally against libstdc++ 15, by replacing the primary template’s using optional<T>::optional; with the four constructors the proposed interface actually has, and adding value_type to both templates. All nine views specified in terms of movable-box still compile and run; __box<Bad> becomes correctly non-default-initializable; and sizeof is unchanged for every box and every view tested, [[no_unique_address]] and the optional base included. No currently conforming program can observe either change: __box is a reserved name, and neither difference is reachable through a view.

So the current libstdc++ result is narrower than “rename plus two mechanical edits.” The interface accidents above can be closed with ABI intact, and the stronger exception specification needs no change, but the public self-assignment guarantee needs either an implementation change or a wording decision. What carries the ABI result is still that the vendor can implement std::movable_box as their box and alias the exposition-only name to it. That is only available because the proposed wording is standalone (Specification shape) and mandates no structure. A specification that made movable_box an optional with patches would put every view’s layout back in play.

The same suite also runs against the other two implementations: libc++’s std::ranges::__movable_box (LLVM 17 through 22) and the Microsoft STL’s std::ranges::_Movable_box (the latest MSVC), built and executed as conformance targets in the reference implementation’s CI [beman.movable_box]. Neither box can host the suite raw. libstdc++’s could, but by an accident of structure: it publicly inherits from optional, which happens to supply every proposed spelling. libc++ and Microsoft wrote theirs freestanding, and each is narrower than the proposed interface in spelling:

Every entry on that list is additive: a member function or a nested typedef, no state, no layout. The conformance targets make that claim executable. The suite runs against a shim deriving from the vendor’s box and adding exactly the missing members, and static assertions check, for every payload type in the suite, that the shim changes neither size nor alignment nor the availability, triviality, or exception specification of any special member. The shim is the candidate adoption patch; the parity checks are the demonstration that it is free.

The semantic divergences that remain fall into two families:

libstdc++’s default-construction accident does not recur: libc++ and the Microsoft STL both constrain their default constructors correctly. One smaller item is specific to the Microsoft STL: its in_place constructor template is unconstrained and non-explicit, so is_constructible_v answers true for argument lists T cannot be constructed from. That is unobservable through the views, and additive to close.

For all three implementations, then, the missing interface is demonstrated additive with layouts unchanged, and the residue is one wording decision plus a conforming strengthening. The self-assignment postcondition of the proposed [movable.box.assign] is provided by none of the three for copyable payloads, and by only libstdc++ for the movable discriminated case, so LEWG should decide it explicitly (Open Questions, item 3): keep the guarantee, with vendors adding one self-check per assignment operator, which has no ABI effect, or weaken the wording to defer to T’s assignment when copyable<T> / movable<T> is modeled, matching everything that ships. The libstdc++ header-patch experiment above remains the deepest of the three results (it also rebuilt every view against the patched box), and repeating it for libc++ and the Microsoft STL is available as further confirmation; but the interface-level question it answered is now answered for all three.

7 Proposed Wording

Wording is relative to the current working draft. The specification is self-contained and follows the drafting style of [optional] and [expected] without referencing either.

7.1 [utility.syn]

Add to the header <utility> synopsis:

// [movable.box], class template movable_box
template<class T>
  requires move_constructible<T> && is_object_v<T>
  class movable_box;

7.2 New subclause [movable.box]

Add a new subclause to [utility] (placement editorial):

Movable wrapper [movable.box]

General [movable.box.general]

1 A movable_box<T> either contains a value of type T or contains no value. When a movable_box<T> contains a value, the contained value is allocated within the storage of the movable_box. Implementations are not permitted to use additional storage, such as dynamic memory, to allocate the contained value.

2 A movable_box contains a value after any of its constructors completes without an exception, unless it was copy or move constructed from a movable_box that contains no value. A movable_box can come to contain no value only as specified for its assignment operators, or by being copy or move constructed from a movable_box containing no value. [Note: A movable_box<T> contains no value only as a consequence of an exception thrown during a copy or move assignment; the state then propagates through subsequent copies, moves, and assignments until a value is assigned. — end note]

3 Recommended practice: If copy_constructible<T> is modeled, movable_box<T> should store only a T if either T models copyable, or is_nothrow_move_constructible_v<T> && is_nothrow_copy_constructible_v<T> is true. Otherwise, movable_box<T> should store only a T if either T models movable or is_nothrow_move_constructible_v<T> is true.

namespace std {
  template<class T>
    requires move_constructible<T> && is_object_v<T>
  class movable_box {
  public:
    using value_type = T;

    // [movable.box.ctor], constructors
    constexpr movable_box() noexcept(is_nothrow_default_constructible_v<T>)
      requires default_initializable<T>;
    constexpr explicit movable_box(const T& t)
      requires copy_constructible<T>;
    constexpr explicit movable_box(T&& t);
    template<class... Args>
      requires is_constructible_v<T, Args...>
    constexpr explicit movable_box(in_place_t, Args&&... args);

    constexpr movable_box(const movable_box& rhs)
      requires copy_constructible<T>;
    constexpr movable_box(movable_box&& rhs) noexcept(
        is_nothrow_move_constructible_v<T>);

    // [movable.box.dtor], destructor
    constexpr ~movable_box();

    // [movable.box.assign], assignment
    constexpr movable_box& operator=(const movable_box& rhs) noexcept(see below)
      requires copy_constructible<T>;
    constexpr movable_box& operator=(movable_box&& rhs) noexcept(see below);

    // [movable.box.obs], observers
    constexpr bool has_value() const noexcept;
    constexpr explicit operator bool() const noexcept;

    constexpr T& operator*() & noexcept;
    constexpr const T& operator*() const& noexcept;
    constexpr T&& operator*() && noexcept;
    constexpr const T&& operator*() const&& noexcept;

    constexpr T* operator->() noexcept;
    constexpr const T* operator->() const noexcept;
  };
}

Constructors [movable.box.ctor]

constexpr movable_box() noexcept(is_nothrow_default_constructible_v<T>)
  requires default_initializable<T>;

1 Effects: Equivalent to movable_box(in_place).

constexpr explicit movable_box(const T& t)
  requires copy_constructible<T>;

2 Effects: Direct-non-list-initializes the contained value with t.

constexpr explicit movable_box(T&& t);

3 Effects: Direct-non-list-initializes the contained value with std::move(t).

template<class... Args> constexpr explicit movable_box(in_place_t, Args&&... args);

4 Constraints: is_constructible_v<T, Args...> is true.

5 Effects: Direct-non-list-initializes the contained value with std::forward<Args>(args)....

constexpr movable_box(const movable_box& rhs)
  requires copy_constructible<T>;

6 Effects: If rhs contains a value, direct-non-list-initializes the contained value with *rhs; otherwise, *this contains no value.

7 Remarks: This constructor is trivial if is_trivially_copy_constructible_v<T> is true.

constexpr movable_box(movable_box&& rhs) noexcept(is_nothrow_move_constructible_v<T>);

8 Effects: If rhs contains a value, direct-non-list-initializes the contained value with std::move(*rhs); otherwise, *this contains no value. rhs.has_value() is unchanged.

9 Remarks: This constructor is trivial if is_trivially_move_constructible_v<T> is true.

Destructor [movable.box.dtor]

constexpr ~movable_box();

1 Effects: Destroys the contained value, if any.

2 Remarks: This destructor is trivial if is_trivially_destructible_v<T> is true.

Assignment [movable.box.assign]

constexpr movable_box& operator=(const movable_box& rhs) noexcept(see below)
  requires copy_constructible<T>;

1 Effects: If this == addressof(rhs) is true, no effects. Otherwise:

  • (1.1) If copyable<T> is modeled: if *this and rhs each contain a value, the contained value of *this is copy-assigned from *rhs; otherwise, if rhs contains a value, a contained value is direct-non-list-initialized with *rhs; otherwise, the contained value of *this, if any, is destroyed.
  • (1.2) Otherwise: the contained value of *this, if any, is destroyed; then, if rhs contains a value, a contained value is direct-non-list-initialized with *rhs. If the initialization exits via an exception, *this contains no value.

2 Returns: *this.

3 Remarks: The exception specification is equivalent to is_nothrow_copy_constructible_v<T> and, if copyable<T> is modeled, is_nothrow_copy_assignable_v<T>.

4 This operator is trivial if is_trivially_copy_constructible_v<T>, is_trivially_copy_assignable_v<T>, and is_trivially_destructible_v<T> are all true.

constexpr movable_box& operator=(movable_box&& rhs) noexcept(see below);

5 Effects: If this == addressof(rhs) is true, no effects. Otherwise:

  • (5.1) If movable<T> is modeled: if *this and rhs each contain a value, the contained value of *this is move-assigned from *rhs; otherwise, if rhs contains a value, a contained value is direct-non-list-initialized with std::move(*rhs); otherwise, the contained value of *this, if any, is destroyed. rhs.has_value() is unchanged.
  • (5.2) Otherwise: the contained value of *this, if any, is destroyed; then, if rhs contains a value, a contained value is direct-non-list-initialized with std::move(*rhs). If the initialization exits via an exception, *this contains no value. rhs.has_value() is unchanged.

6 Returns: *this.

7 Remarks: The exception specification is equivalent to is_nothrow_move_constructible_v<T> and, if movable<T> is modeled, is_nothrow_move_assignable_v<T>.

8 This operator is trivial if is_trivially_move_constructible_v<T>, is_trivially_move_assignable_v<T>, and is_trivially_destructible_v<T> are all true.

Observers [movable.box.obs]

constexpr bool has_value() const noexcept;
constexpr explicit operator bool() const noexcept;

1 Returns: true if and only if *this contains a value.

constexpr T& operator*() & noexcept;
constexpr const T& operator*() const& noexcept;

2 Preconditions: *this contains a value.

3 Returns: A reference to the contained value.

constexpr T&& operator*() && noexcept;
constexpr const T&& operator*() const&& noexcept;

4 Preconditions: *this contains a value.

5 Returns: std::move(the contained value).

constexpr T* operator->() noexcept;
constexpr const T* operator->() const noexcept;

6 Preconditions: *this contains a value.

7 Returns: addressof(the contained value).

Drafting note: paragraphs [movable.box.assign] 1.1/1.2 and 5.1/5.2 reproduce, in standalone form, the semantics of [range.move.wrap] (1.3)/(1.4) together with the optional assignment semantics those paragraphs defer to when copyable<T>/movable<T> are modeled. Three of the four combined exception specifications reduce to the current ones: is_nothrow_copy_constructible_v<T> when copyable<T> is not modeled, is_nothrow_move_constructible_v<T> when movable<T> is not, and optional’s own is_nothrow_move_constructible_v<T> && is_nothrow_move_assignable_v<T> when it is. The fourth does not: when copyable<T> is modeled the current wording defers to optional’s copy assignment, which carries no exception specification at all, so [movable.box.assign] p3 strengthens it. The strengthening is deliberate, and is one an implementation was already free to make under [res.on.exception.handling]; no currently conforming program can observe the difference. LWG review should confirm the other three equivalences and this one departure. The triviality Remarks on the assignment operators follow the conditions of [optional.assign] and keep movable_box<T> (and every view storing one) trivially copyable for trivially copyable, trivially assignable T; this is new relative to the exposition-only specification, whose triviality Remarks cover only the constructors and the destructor, and the reference implementation implements it.

The constructor triviality Remarks hold whether or not the box carries a discriminant. Trivial copy and move constructors cannot throw, but a T with one of them trivial can still need the empty state when the other may throw: a non-assignable T with a trivial copy constructor and a potentially-throwing move constructor, for instance. The box’s copy constructor must then be trivial in the discriminated layout too. Copying the contained value and the discriminant as they stand gives the specified effects whether or not a value is present, so triviality holds in both layouts. optional’s constructors already behave this way, and both libstdc++ and libc++ inherit it by falling back to optional; the reference implementation defaults the corresponding constructor directly.

7.3 [range.move.wrap]

Replace the contents of [range.move.wrap] as follows:

1 Many types in this subclause are specified in terms of an exposition-only class template movable-box. movable-box<T> denotes std::movable_box<T> ([movable.box]).

1 movable-box<T> behaves exactly like optional<T> with the following differences: […]

2 Recommended practice: […]

Drafting note: paragraphs 1.1–1.4 and 2 are struck in their entirety; their content is subsumed by [movable.box]. All uses of movable-box elsewhere in [ranges] are unchanged, as are the semantics of every view specified in terms of it. Clause [ranges] performs these operations on movable-box: default construction, direct-initialization from const T& and T&&, in_place construction, the special member functions, operator*, operator-> (single_view::data), and has_value in preconditions. All are provided by [movable.box]; this claim should be re-audited against the working draft during wording review.

7.4 [version.syn]

Add:

#define __cpp_lib_movable_box 20XXXXL // also in <utility>

8 Open Questions for LEWG

  1. Header: <utility> or a new <movable_box> (Header)?
  2. Namespace: std or std::ranges (Namespace)?
  3. Self-assignment: the proposed [movable.box.assign] specifies no effects when this == addressof(rhs). No shipping implementation provides that where the box defers to T’s own assignment operator (see Implementation Experience). Keep the guarantee (vendors add a self-check per assignment operator) or defer to T’s assignment semantics when copyable<T> / movable<T> is modeled, matching what ships?

9 Acknowledgements

Thanks to Eric Niebler for semiregular_box and the ranges design, and to the authors of [P2325R3] and [P2494R2], whose work converged this type to its current, stable form.

10 References

[beman.movable_box] Steve Downey. beman.movable_box: A non-exposition-only movable-box.
https://github.com/bemanproject/movable_box
[P2325R3] Barry Revzin. 2021-05-14. Views should not be required to be default constructible.
https://wg21.link/p2325r3
[P2494R2] Michał Dominiak. 2022-07-13. Relaxing range adaptors to allow for move only types.
https://wg21.link/p2494r2
[P3019R11] Jonathan Coe, Antony Peacock, Sean Parent. 2024-11-23. Vocabulary Types for Composite Class Design.
https://wg21.link/p3019r11
[range.move.wrap] Working Draft, Programming Languages — C++, [range.move.wrap].
https://eel.is/c++draft/range.move.wrap
[range-v3] Eric Niebler. range-v3: Range library for C++14/17/20.
https://github.com/ericniebler/range-v3