Skip to content

Latest commit

 

History

History
1511 lines (1153 loc) · 60.8 KB

File metadata and controls

1511 lines (1153 loc) · 60.8 KB

rvariantGitHub

rvariant is a standard-compatible variant library that supports recursive types.

// A common pattern for representing recursive ASTs using recursive variants.
struct BinaryExpr;
using Expr = iris::rvariant<int, double, iris::recursive_wrapper<BinaryExpr>>;
enum class Op;
struct BinaryExpr { Expr lhs, rhs; Op op{}; };

Expr expr{BinaryExpr{Expr{42}, Expr{3.14}}};
expr.visit(iris::overloaded{
    [](int const&) { /* ... */ },
    [](double const&) { /* ... */ },
    [](BinaryExpr const&) { /* ... */ },
});

Supported Environments

  • GCC 14

  • Clang 21-22 (libc++)

  • MSVC 2022 and 2026

  • C++23 and C++26

Introduction

Motivation

Since its introduction in 2004, boost::variant has been used for a wide range of type-safe union use cases.

Starting with C++17, most of those can be replaced with std::variant, except for recursive types. As a result, many users of generic frameworks that require recursive variants—​most notably Boost.Spirit—​have continued using boost::variant, despite its significant impact on compile times.

The compile-time slowness of boost::variant stems from long-standing technical debt: it relies heavily on preprocessor magic in Boost.MPL. This wizardry is so tightly coupled with boost::variant's internals that any attempt to modernize it would be unrealistic; it would effectively require a complete rewrite.

Until 2025, no one had managed to introduce a modern alternative to either Boost or the C++ standard. rvariant fills this gap with a new implementation that supports recursive types while remaining API-compatible with std::variant.

Project Goals

  1. Provide a modern, efficient, and battle-tested recursive variant library to enable developers to avoid using Boost.Variant in new applications.

  2. Replace existing usages of Boost.Variant in established frameworks.

  3. Explore potential directions for future standardization, while gaining implementation experience with the modernized design.

Comparison of Variant Libraries

rvariant

std::variant

Boost.Variant

Boost.Variant2

Minimum C++ version

C++23

C++17

C++11
[warning] Mostly legacy code from 2003

C++11

Compilation speed

Average

Average

Very Slow

Average

Interface for recursive types

Yes

No

Yes

No

May be valueless?

Yes

Yes

No

No

Exception safety

Basic exception safety

Basic exception safety

Strong exception safety
(Temporary heap backup)

Strong exception safety
(Double storage)

Flexible construction
V<A,B> = V<B,A>
V<A,B,C> = V<A,B>

Yes

No

Yes

Yes

Rationale

Why is recursive_wrapper needed?

A recursive type is effectively an incomplete type at the point of its initial definition. However, a std-style variant class requires all alternatives to be complete, since the storage size must be determined at compile time. As a result, recursive alternatives must be wrapped in a recursive wrapper class, which holds the incomplete type via a pointer and manages it through dynamic memory allocation.

Why can’t I just use std::unique_ptr instead of recursive_wrapper?

Theoretically, a recursive wrapper class can be designed independently, i.e., with no relation to the variant class itself. So there’s no theoretical reason you couldn’t use std::unique_ptr with std::variant.

However, it is essential to have compatible interfaces in the variant class, because the lack of interfaces means that you must:

  • wrap the actual type with the wrapper class every time you modify variants, and

  • unwrap the instance every time you access an alternative with get() or visit().

In other words, the commonly seen advice like "use std::unique_ptr if you want to hold recursive types in std::variant" is a hack that basically does nothing except for just holding the wrapper instance. As noted above, such unadapted wrappers introduce intrusive boilerplate that spreads to the application layer, which is undesirable in practice.

Why can’t rvariant be a thin wrapper around std::variant?

rvariant is designed to be a strict superset of std::variant, not just to act as a third-party drop-in replacement.

If rvariant is specified correctly, it has the potential to become the only variant library in the C++ standard capable of handling both recursive and non-recursive types transparently. However, if it is designed merely as a thin wrapper around std::variant (e.g., inheritance or composition), such a specification could never be standardized.

For this purpose, the documentation of rvariant is presented in a diff-based format that mirrors std::variant, with key characteristics marked with underlines.

Feature Cheat Sheet

This section shows the pseudo code for all features in this library. You can click on the links to jump to the corresponding reference.

Basic Features

using A = int;
using B = double;
struct C {};

using AB  = iris::rvariant<A, B>;
using BA  = iris::rvariant<B, A>;
using ABC = iris::rvariant<A, B, C>;

// constructor
AB ab{42};

{
    AB local_ab{};
} // destructor

ab.emplace<int>(123);
ab.emplace<0>(123);

// assignment
ab = AB{123};

A& a = iris::get<0>(ab);
A& a = iris::get<A>(ab);
C& c = iris::get<C>(ab);     // throws std::bad_variant_access

A* a = iris::get_if<0>(&ab);
A* a = iris::get_if<A>(&ab);
C* c = iris::get_if<C>(&ab); // nullptr

// compatibility with boost; same effect as get_if
A* a = iris::get<0>(&ab);
A* a = iris::get<A>(&ab);
C* c = iris::get<C>(&ab);    // nullptr

ab == ab;
ab < ab;
ab <=> ab; // requires (std::three_way_comparable<Ts> && ...)

auto visitor = iris::overloaded {
    [](A const& a) {},
    [](B const& b) {},
};

ab.visit(visitor);           // member visit
iris::visit(visitor, ab); // function visit

Advanced Features

{
    // flexible construction
    AB ab{BA{}};   // unordered construction
    ABC abc{AB{}}; // subset construction

    // flexible assignment
    ab = BA{};     // unordered assignment
    abc = AB{};    // subset assignment
}

static_assert(iris::variant_size_v<AB> == 2);
static_assert(std::same_as<iris::variant_alternative_t<0, AB>, A>);
static_assert(iris::holds_alternative<A>(ab));

static_assert(!ab.valueless_by_exception());
static_assert(ab.index() != std::variant_npos);

{
    AB tmp;
    ab.swap(tmp);
}
{
    using std::swap;
    AB tmp;
    swap(ab, tmp); // ADL
}

std::size_t _ = std::hash<AB>{ab}();
std::size_t _ = hash_value(ab); // compatibility with boost

// I/O support
{
    using V = ::iris::rvariant<int, double>;

    // operator<< support
    std::cout << V{42} << '\n';  // prints 42

    // std::formatter support
    std::println("{}", V{42});   // prints 42

    constexpr auto v_fmt = iris::variant_format_for<V>("{:04d}", "{:.1f}");
    std::println("foo{}bar", iris::format_by(v_fmt, V(42)); // prints foo0042bar
    std::println("foo{}bar", iris::format_by(v_fmt, V(3.14)); // prints foo3.1bar
}

Reference [rvariant]

Header <iris/rvariant.hpp> synopsis [rvariant.syn]

namespace iris {

// [rvariant.rvariant], class template rvariant
template<class... Ts>
class rvariant;

// [rvariant.recursive], class template recursive_wrapper
template<class T>
class recursive_wrapper;
template<class T, class Allocator = std::allocator<T>>
class recursive_wrapper_alloca;

/* all features commented below defined as per [variant] */
    // variant_size, variant_size_v
    // operator==
    // operator!=
    // operator<
    // operator>
    // operator<=
    // operator>=
    // operator<=>
    // swap

/* not defined; use the std:: versions instead */
    // variant_npos
    // monostate and monostate-related functionalities
    // std::bad_variant_access

// [rvariant.helper], rvariant helper classes
template<std::size_t I, class T> struct variant_alternative; // not defined
template<std::size_t I, class T> struct variant_alternative<I, T const>;
template<std::size_t I, class T>
  using variant_alternative_t = typename variant_alternative<I, T>::type;

template<std::size_t I, class... Ts>
  struct variant_alternative<I, rvariant<Ts...>>;

// [rvariant.get], value access
template<class T, class... Ts>
  constexpr bool holds_alternative(rvariant<Ts...> const&) noexcept;

template<std::size_t I, class... Ts>
  constexpr variant_alternative_t<I, rvariant<Ts...>>&
    get(rvariant<Ts...>&);
template<std::size_t I, class... Ts>
  constexpr variant_alternative_t<I, rvariant<Ts...>>&&
    get(rvariant<Ts...>&&);
template<std::size_t I, class... Ts>
  constexpr variant_alternative_t<I, rvariant<Ts...>> const&
    get(rvariant<Ts...> const&);
template<std::size_t I, class... Ts>
  constexpr variant_alternative_t<I, rvariant<Ts...>> const&&
    get(rvariant<Ts...> const&&);

template<class T, class... Ts> constexpr T&        get(rvariant<Ts...>&);
template<class T, class... Ts> constexpr T&&       get(rvariant<Ts...>&&);
template<class T, class... Ts> constexpr T const&  get(rvariant<Ts...> const&);
template<class T, class... Ts> constexpr T const&& get(rvariant<Ts...> const&&);

template<std::size_t I, class... Ts>
  constexpr std::add_pointer_t<variant_alternative_t<I, rvariant<Ts...>>>
    get_if(rvariant<Ts...>*) noexcept;
template<std::size_t I, class... Ts>
  constexpr std::add_pointer_t<variant_alternative_t<I, rvariant<Ts...>> const>
    get_if(rvariant<Ts...> const*) noexcept;

template<class T, class... Ts>
  constexpr std::add_pointer_t<T>
    get_if(rvariant<Ts...>*) noexcept;
template<class T, class... Ts>
  constexpr std::add_pointer_t<T const>
    get_if(rvariant<Ts...> const*) noexcept;

// [rvariant.visit], visitation
template<class Visitor, class... Variants>
  constexpr see below visit(Visitor&&, Variants&&...);
template<class R, class Visitor, class... Variants>
  constexpr R visit(Visitor&&, Variants&&...);

// [rvariant.hash], hash support
template<class... Ts>
  /* constexpr */ std::size_t hash_value(rvariant<Ts...> const&);

// [rvariant.hash], hash support
template<class T>
  /* constexpr */ std::size_t hash_value(recursive_wrapper<T> const&);
template<class T, class Allocator>
  /* constexpr */ std::size_t hash_value(recursive_wrapper_alloca<T, Allocator> const&);

// [rvariant.recursive.helper], recursive_wrapper helper classes
template<class T> using unwrap_recursive_type = see below;
template<class T> constexpr auto&& unwrap_recursive(T&& o) noexcept;

} // iris
namespace std {

// [rvariant.hash], hash support
template<class... Ts> struct hash<::iris::rvariant<Ts...>>;

// [rvariant.hash], hash support
template<class T, class Allocator> struct hash<::iris::recursive_wrapper<T, Allocator>>;

} // std

Class template rvariant [rvariant.rvariant]

namespace iris {

template<class... Ts>
class rvariant
{
public:
  // [rvariant.ctor], constructors
  constexpr rvariant::rvariant() noexcept(see below);
  constexpr rvariant::rvariant(rvariant const&);
  constexpr rvariant::rvariant(rvariant&&) noexcept(see below);

  template<class T>
    constexpr rvariant(T&&) noexcept(see below);

  template<class T, class... Args>
    constexpr explicit rvariant(std::in_place_type_t<T>, Args&&...);
  template<class T, class U, class... Args>
    constexpr explicit rvariant(std::in_place_type_t<T>, std::initializer_list<U>, Args&&...);
  template<std::size_t I, class... Args>
    constexpr explicit rvariant(std::in_place_index_t<I>, Args&&...);
  template<std::size_t I, class U, class... Args>
    constexpr explicit rvariant(std::in_place_index_t<I>, std::initializer_list<U>, Args&&...);

  // [rvariant.ctor], flexible constructors
  template<class... Us>
    constexpr rvariant(rvariant<Us...> const&);
  template<class... Us>
    constexpr rvariant(rvariant<Us...>&&) noexcept(see below);

  // [rvariant.dtor], destructor
  constexpr ~rvariant();

  // [rvariant.assign], assignment
  constexpr rvariant& operator=(rvariant const&);
  constexpr rvariant& operator=(rvariant&&) noexcept(see below);

  template<class T> constexpr rvariant& operator=(T&&) noexcept(see below);

  // [rvariant.assign], flexible assignment
  template<class... Us>
    constexpr rvariant& operator=(rvariant<Us...> const&);
  template<class... Us>
    constexpr rvariant& operator=(rvariant<Us...>&&) noexcept(see below);

  // [rvariant.mod], modifiers
  template<class T, class... Args>
    constexpr T& emplace(Args&&...);
  template<class T, class U, class... Args>
    constexpr T& emplace(std::initializer_list<U>, Args&&...);
  template<std::size_t I, class... Args>
    constexpr variant_alternative_t<I, rvariant<Ts...>>& emplace(Args&&...);
  template<std::size_t I, class U, class... Args>
    constexpr variant_alternative_t<I, rvariant<Ts...>>&
      emplace(std::initializer_list<U>, Args&&...);

  // [rvariant.status], value status
  constexpr bool valueless_by_exception() const noexcept;
  constexpr std::size_t index() const noexcept;

  // [rvariant.subset], subset
  template<class... Us>
    constexpr rvariant<Us...> subset() const& noexcept(see below);
  template<class... Us>
    constexpr rvariant<Us...> subset() && noexcept(see below);

  // [rvariant.swap], swap
  constexpr void swap(rvariant&) noexcept(see below);

  // [rvariant.visit], visitation
  template<class Self, class Visitor>
    constexpr decltype(auto) visit(this Self&&, Visitor&&);
  template<class R, class Self, class Visitor>
    constexpr R visit(this Self&&, Visitor&&);
};

} // iris
Note
See also: spec of std::variant and boost::variant
  • Class template rvariant follows all requirements of std::variant, unless otherwise noted.

  • All types in Ts must satisfy all requirements on the corresponding parameter in std::variant, unless otherwise noted.

  • Let T denote an arbitrary type. For the template parameter of rvariant, if a user provides both T and any instantiation of recursive_wrapper or recursive_wrapper_alloca that has the value_type of T, the program is ill-formed.

  • Let T denote an arbitrary type. For the template parameter of rvariant, if a user provides multiple different instantiations of recursive_wrapper_alloca such that the first template parameter is T, the program is ill-formed.

Warning

Although rvariant is normally capable of holding duplicate alternatives, the above restriction exists for preventing error-prone instantiation of rvariant:

rvariant<
  int, recursive_wrapper<int>, recursive_wrapper_alloca<int, MyAllocator<int>>
> v(42); // error-prone; not allowed
  • If recursive_wrapper<Ti> or recursive_wrapper_alloca<Ti, A> (with any type A) occurs anywhere in Ts..., let VTi denote that type; otherwise, let VTi denote Ti. Let Uj denote the jth type of the template parameter pack having the name Us on each flexibility-related functions. The corresponding alternative for rvariant is the first type for which std::is_same_v<unwrap_recursive_type<VTi>, unwrap_recursive_type<Uj>> is true.

  • For the function that has the formal template parameter named T: if recursive_wrapper<T> or recursive_wrapper_alloca<T, A> (with any type A) occurs anywhere in Ts..., let VT denote that type; otherwise, let VT denote T.

Constructors [rvariant.ctor]

constexpr rvariant::rvariant() noexcept(see below);// 1
constexpr rvariant::rvariant(rvariant const& w);// 2
constexpr rvariant::rvariant(rvariant&& w) noexcept(see below);// 3

// Generic constructor
template<class T>
constexpr rvariant(T&& t) noexcept(see below);// 4


template<class T, class... Args>
constexpr explicit rvariant(std::in_place_type_t<T>, Args&&... args);// 5

template<class T, class U, class... Args>
constexpr explicit rvariant(std::in_place_type_t<T>, std::initializer_list<U> il, Args&&...);// 6

template<std::size_t I, class... Args>
constexpr explicit rvariant(std::in_place_index_t<I>, Args&&... args);// 7

template<std::size_t I, class U, class... Args>
constexpr explicit rvariant(std::in_place_index_t<I>, std::initializer_list<U> il, Args&&... args);// 8


// Flexible copy constructor
template<class... Us>
constexpr rvariant(rvariant<Us...> const& w);// 9

// Flexible move constructor
template<class... Us>
constexpr rvariant(rvariant<Us...>&& w) noexcept(see below);// 10
  • 1-3) _std-variant-proxy.adoc

  • 4) Generic constructor. Equivalent to the std::variant counterpart, [spec] except:

    Postconditions: holds_alternative<unwrap_recursive_type<Tj>>(*this) is true.

  • 5) Mandates: T is not a specialization of recursive_wrapper or recursive_wrapper_alloca.

    Constraints:

    •  — There is exactly one occurrence of T in unwrap_recursive_type<Ts>... and

    •  — std::is_constructible_v<VT, Args...> is true.

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

    Postconditions: holds_alternative<T>(*this) is true.

    Throws: Any exception thrown by calling the selected constructor of VT.

    Remarks: If VT's selected constructor is a constexpr constructor, this constructor is a constexpr constructor.

  • 6) Mandates: T is not a specialization of recursive_wrapper or recursive_wrapper_alloca.

    Constraints:

    •  — There is exactly one occurrence of T in unwrap_recursive_type<Ts>... and

    •  — std::is_constructible_v<VT, std::initializer_list<U>&, Args...> is true.

    Effects: Direct-non-list-initializes the contained value of type VT with il, std::forward<Args>(args)....

    Postconditions: holds_alternative<T>(*this) is true.

    Throws: Any exception thrown by calling the selected constructor of VT.

    Remarks: If VT's selected constructor is a constexpr constructor, this constructor is a constexpr constructor.

  • 7-8) _std-variant-proxy.adoc

  • 9) Flexible copy constructor.

    Let VTi and Uj denote the types defined in [rvariant.rvariant.general].

    Constraints:

    •  — std::is_same_v<rvariant<Us...>, rvariant> is false, and

    •  — rvariant_set::subset_of<rvariant<Us...>, rvariant> is true, and

    •  — std::disjunction_v<std::is_same<rvariant<Us...>, unwrap_recursive_type<Ts>>...> is false, and

    •  — std::is_constructible_v<VTi, Uj const&> is true for all j.

    Effects: If w holds a value, initializes the rvariant to hold VTi (with i being the index of the alternative corresponding to that of w) and direct-initializes the contained value with GET<w.index()>(w). Otherwise, initializes the rvariant to not hold a value.

    Throws: Any exception thrown by direct-initializing any alternative corresponding to that of w.

    Remarks:

    •  — The exception specification is equivalent to the logical AND of std::is_nothrow_constructible_v<VTi, Uj const&> for all j.

  • 10) Flexible move constructor.

    Let VTi and Uj denote the types defined in [rvariant.rvariant.general].

    Constraints:

    •  — std::is_same_v<rvariant<Us...>, rvariant> is false, and

    •  — rvariant_set::subset_of<rvariant<Us...>, rvariant> is true, and

    •  — std::disjunction_v<std::is_same<rvariant<Us...>, unwrap_recursive_type<Ts>>...> is false, and

    •  — std::is_constructible_v<VTi, Uj&&> is true for all j.

    Effects: If w holds a value, initializes the rvariant to hold VTi (with i being the index of the alternative corresponding to that of w) and direct-initializes the contained value with GET<w.index()>(std::move(w)). Otherwise, initializes the rvariant to not hold a value.

    Throws: Any exception thrown by move-constructing any alternative corresponding to that of w.

    Remarks:

    •  — The exception specification is equivalent to the logical AND of std::is_nothrow_constructible_v<VTi, Uj&&> for all j.

Destructor [rvariant.dtor]

constexpr ~rvariant();// 1
constexpr rvariant& operator=(rvariant const& rhs);// 1
constexpr rvariant& operator=(rvariant&& rhs) noexcept(see below);// 2

// Generic assignment operator
template<class T>
constexpr rvariant& operator=(T&& t) noexcept(see below);// 3

// Flexible copy assignment operator
template<class... Us>
constexpr rvariant& operator=(rvariant<Us...> const& rhs);// 4

// Flexible move assignment operator
template<class... Us>
constexpr rvariant& operator=(rvariant<Us...>&& rhs) noexcept(see below);// 5
  • 1-2) _std-variant-proxy.adoc

  • 3) Generic assignment operator. Equivalent to the std::variant counterpart, [spec] except:

    Postconditions: holds_alternative<unwrap_recursive_type<Tj>>(*this) is true, with Tj selected by the imaginary function overload resolution described above.

  • 4) Flexible copy assignment operator.

    Let VTi and Uj denote the types defined in [rvariant.rvariant.general].

    Constraints:

    •  — std::is_same_v<rvariant<Us...>, rvariant> is false, and

    •  — rvariant_set::subset_of<rvariant<Us...>, rvariant> is true, and

    •  — std::disjunction_v<std::is_same<rvariant<Us...>, unwrap_recursive_type<Ts>>...> is false, and

    •  — std::is_constructible_v<VTi, Uj const&> && std::is_assignable_v<VTi&, Uj const&> is true for all j.

    Effects: Let j be rhs.index().

    •  — If neither *this nor rhs holds a value, there is no effect.

    •  — Otherwise, if *this holds a value but rhs does not, destroys the value contained in *this and sets *this to not hold a value.

    •  — Otherwise, if rhs holds a value but *this does not, initializes rvariant to hold VTi (with i being the index of the alternative corresponding to that of rhs) and direct-initializes the contained value with GET<j>(rhs).

    •  — Otherwise, if std::is_same_v<unwrap_recursive_type<Ti>, unwrap_recursive_type<Uj>> is true, assigns GET<j>(rhs) to the value contained in *this. (Note: the left hand side is Ti, not VTi. This ensures that the existing storage is reused even for rvariant with duplicate corresponding alternatives; i.e., index() is unchanged.)

    •  — Otherwise, if either std::is_nothrow_constructible_v<VTi, Uj const&> is true or std::is_nothrow_move_constructible_v<VTi> is false, equivalent to emplace<VTi>(GET<j>(rhs)).

    •  — Otherwise, equivalent to emplace<VTi>(VTi(GET<j>(rhs))).

    Postconditions: If rhs.valueless_by_exception() is true, index() == rhs.index(); otherwise, *this holds the alternative corresponding to that of rhs.

    Returns: *this.

    Remarks: The exception specification is equivalent to the logical AND of std::is_nothrow_constructible_v<VTi, Uj const&> && std::is_nothrow_assignable_v<VTi&, Uj const&> for all j.

  • 5) Flexible move assignment operator.

    Let VTi and Uj denote the types defined in [rvariant.rvariant.general].

    Constraints:

    •  — std::is_same_v<rvariant<Us...>, rvariant> is false,

    •  — rvariant_set::subset_of<rvariant<Us...>, rvariant> is true, and

    •  — std::disjunction_v<std::is_same<rvariant<Us...>, unwrap_recursive_type<Ts>>...> is false, and

    •  — std::is_constructible_v<VTi, Uj&&> && std::is_assignable_v<VTi&, Uj&&> is true for all j.

    Effects: Let j be rhs.index().

    •  — If neither *this nor rhs holds a value, there is no effect.

    •  — Otherwise, if *this holds a value but rhs does not, destroys the value contained in *this and sets *this to not hold a value.

    •  — Otherwise, if rhs holds a value but *this does not, initializes rvariant to hold VTi (with i being the index of the alternative corresponding to that of rhs) and direct-initializes the contained value with GET<j>(std::move(rhs)).

    •  — Otherwise, if std::is_same_v<unwrap_recursive_type<Ti>, unwrap_recursive_type<Uj>> is true, assigns GET<j>(std::move(rhs)) to the value contained in *this. (Note: the left hand side is Ti, not VTi. This ensures that the existing storage is reused even for rvariant with duplicate corresponding alternatives; i.e., index() is unchanged.)

    •  — Otherwise, equivalent to emplace<VTi>(GET<j>(std::move(rhs))).

    Returns: *this.

    Remarks: The exception specification is equivalent to the logical AND of std::is_nothrow_constructible_v<VTi, Uj&&> && std::is_nothrow_assignable_v<VTi&, Uj&&> for all j.

Modifiers [rvariant.mod]

template<class T, class... Args>
constexpr T& emplace(Args&&... args);// 1

template<class T, class U, class... Args>
constexpr T& emplace(std::initializer_list<U> il, Args&&... args);// 2

template<std::size_t I, class... Args>
constexpr variant_alternative_t<I, rvariant<Ts...>>&
  emplace(Args&&... args);// 3

template<std::size_t I, class U, class... Args>
constexpr variant_alternative_t<I, rvariant<Ts...>>&
  emplace(std::initializer_list<U> il, Args&&... args);// 4
  • 1) Mandates: T is not a specialization of recursive_wrapper or recursive_wrapper_alloca.

    Constraints: std::is_constructible_v<VT, Args...> is true, and T occurs exactly once in unwrap_recursive_type<Ts>.

    Effects: Equivalent to:
      return emplace<I>(std::forward<Args>(args)...);
    where I is the zero-based index of T in unwrap_recursive_type<Ts>.

  • 2) Mandates: T is not a specialization of recursive_wrapper or recursive_wrapper_alloca.

    Constraints: std::is_constructible_v<VT, std::initializer_list<U>&, Args...> is true, and T occurs exactly once in unwrap_recursive_type<Ts>.

    Effects: Equivalent to:
      return emplace<I>(il, std::forward<Args>(args)...);
    where I is the zero-based index of T in unwrap_recursive_type<Ts>.

  • 3) Equivalent to the std::variant counterpart, [spec] except:

    Returns: Let o denote a reference to the new contained value. Returns unwrap_recursive(o).

    Remarks: If TI is a specialization of recursive_wrapper or recursive_wrapper_alloca, this function is permitted to construct an intermediate variable tmp as if by passing std::forward<Args>(args)... to TI's constructor. Then rvariant direct-non-list-initializes the contained value of TI with the argument std::move(tmp). (Note: This allows optimization where rvariant can be assumed to become never valueless on certain cases.)

  • 4) Equivalent to the std::variant counterpart, [spec] except:

    Returns: Let o denote a reference to the new contained value. Returns unwrap_recursive(o).

    Remarks: If TI is a specialization of recursive_wrapper or recursive_wrapper_alloca, this function is permitted to construct an intermediate variable tmp as if by passing il, std::forward<Args>(args)... to TI's constructor. Then rvariant direct-non-list-initializes the contained value of TI with the argument std::move(tmp). (Note: This allows optimization where rvariant can be assumed to become never valueless on certain cases.)

Value status [rvariant.status]

constexpr bool valueless_by_exception() const noexcept;
constexpr std::size_t index() const noexcept;
template<class... Us>
  requires std::is_same_v<rvariant<Us...>, rvariant>
constexpr rvariant subset() const& noexcept(std::is_nothrow_copy_constructible_v<rvariant>);// 1

template<class... Us>
  requires std::is_same_v<rvariant<Us...>, rvariant>
constexpr rvariant subset() && noexcept(std::is_nothrow_move_constructible_v<rvariant>);// 2

template<class... Us>
  requires (!std::is_same_v<rvariant<Us...>, rvariant>)
constexpr rvariant<Us...> subset() const& noexcept(see below);// 3

template<class... Us>
  requires (!std::is_same_v<rvariant<Us...>, rvariant>)
constexpr rvariant<Us...> subset() && noexcept(see below);// 4
  • 1) Returns: *this.

    Throws: Any exception thrown by copy-constructing any type in Us.

  • 2) Returns: std::move(*this).

    Throws: Any exception thrown by move-constructing any type in Us.

  • 3) Mandates: std::is_copy_constructible_v<Uj> is true for all j, where Uj be the jth type in Us.

    Effects: If *this holds a value, returns an rvariant<Us...> object that holds the alternative corresponding to that of *this, with its contained value direct-initialized from GET<i>(*this), where i is this->index(). Otherwise, returns an rvariant<Us...> object that does not hold a value.

    Throws: std::bad_variant_access if *this holds an alternative that is not contained in Us; otherwise, equivalent to the semantics of the flexible copy constructor.

    Remarks:

    •  — This function does not participate in overload resolution unless rvariant_set::subset_of<rvariant<Us...>, rvariant> is true.

    •  — The exception specification is equivalent to the logical AND of rvariant_set::equivalent_to<rvariant<Us...>, rvariant> and std::is_nothrow_constructible_v<rvariant<Us...>, rvariant const&>.

    •  — The corresponding index on the returned rvariant<Us...> object shall be determined according to the rules defined in the flexible copy constructor.

  • 4) Equivalent to the overload #3, except:

    •  — Citation of flexible copy constructor is replaced with flexible move constructor.

    Mandates: std::is_move_constructible_v<Uj> is true for all j, where Uj be the jth type in Us.

    Effects: GET<i>(*this) is replaced with GET<i>(std::move(*this)).

    Remarks: std::is_nothrow_constructible_v<rvariant<Us...>, rvariant const&> is replaced with std::is_nothrow_constructible_v<rvariant<Us...>, rvariant&&>.

constexpr void swap(rvariant&) noexcept(see below);

rvariant helper classes [rvariant.helper]

namespace iris {

template<std::size_t I, class T>
struct variant_alternative; // not defined

template<std::size_t I, class T>
struct variant_alternative<I, T const>;// 1

template<std::size_t I, class... Ts>
struct variant_alternative<I, rvariant<Ts...>>;// 2

} // iris

Flexibility traits [rvariant.flex]

namespace iris::rvariant_set {

template<class W, class V>
struct is_subset_of : std::false_type {};// 1

template<class... Us, class... Ts>
struct is_subset_of<rvariant<Us...>, rvariant<Ts...>>;// 2

template<class W, class V>
constexpr bool is_subset_of_v = is_subset_of<W, V>::value;

template<class W, class V>
concept subset_of = is_subset_of_v<W, V>;

template<class W, class V>
concept equivalent_to = subset_of<W, V> && subset_of<V, W>;

} // iris::rvariant_set
  • 1) Mandates: Both W and V are specialization of rvariant.

  • 2) Constraints: For every type U in Us, there exists at least one type T in Ts such that:

Value access [rvariant.get]

namespace iris {

template<class T, class... Ts>
constexpr bool holds_alternative(rvariant<Ts...> const& v) noexcept;

} // iris
template<std::size_t I, class... Ts>
constexpr see below& GET(rvariant<Ts...>& v);               // exposition only

template<std::size_t I, class... Ts>
constexpr see below&& GET(rvariant<Ts...>&& v);             // exposition only

template<std::size_t I, class... Ts>
constexpr see below const& GET(rvariant<Ts...> const& v);   // exposition only

template<std::size_t I, class... Ts>
constexpr see below const&& GET(rvariant<Ts...> const&& v); // exposition only
namespace iris {

template<std::size_t I, class... Ts>
constexpr variant_alternative_t<I, rvariant<Ts...>>&
  get(rvariant<Ts...>& v);

template<std::size_t I, class... Ts>
constexpr variant_alternative_t<I, rvariant<Ts...>>&&
  get(rvariant<Ts...>&& v);

template<std::size_t I, class... Ts>
constexpr variant_alternative_t<I, rvariant<Ts...>> const&
  get(rvariant<Ts...> const& v);

template<std::size_t I, class... Ts>
constexpr variant_alternative_t<I, rvariant<Ts...>> const&&
  get(rvariant<Ts...> const&& v);

} // iris
  • Mandates: I < sizeof...(Ts).

    Effects: If v.index() is I, returns unwrap_recursive(o), where o denotes a reference to the object stored in the rvariant. Otherwise, throws an exception of type std::bad_variant_access.

namespace iris {

template<class T, class... Ts> constexpr T&        get(rvariant<Ts...>& v);
template<class T, class... Ts> constexpr T&&       get(rvariant<Ts...>&& v);
template<class T, class... Ts> constexpr T const&  get(rvariant<Ts...> const& v);
template<class T, class... Ts> constexpr T const&& get(rvariant<Ts...> const&& v);

} // iris
namespace iris {

template<std::size_t I, class... Ts>
constexpr std::add_pointer_t<variant_alternative_t<I, rvariant<Ts...>>>
  get_if(rvariant<Ts...>*) noexcept;// 1

template<std::size_t I, class... Ts>
constexpr std::add_pointer_t<variant_alternative_t<I, rvariant<Ts...>> const>
  get_if(rvariant<Ts...> const* v) noexcept;// 2

} // iris
  • 1-2) Mandates: I < sizeof...(Ts).

    Returns: A pointer to the value denoted by unwrap_recursive(o), where o denotes a reference to the object stored in the rvariant, if v != nullptr and v->index() == I. Otherwise, returns nullptr.

namespace iris {

template<class T, class... Ts>
constexpr std::add_pointer_t<T>
  get_if(rvariant<Ts...>* v) noexcept;// 1

template<class T, class... Ts>
constexpr std::add_pointer_t<T const>
  get_if(rvariant<Ts...> const* v) noexcept;// 2

} // iris

Visitation [rvariant.visit]

namespace iris {

template<class Visitor, class... Variants>
constexpr see below visit(Visitor&& vis, Variants&&... vars);// 1

template<class R, class Visitor, class... Variants>
constexpr R visit(Visitor&& vis, Variants&&... vars);// 2

} // iris

// below are member functions of the class template rvariant:

template<class Self, class Visitor>
constexpr decltype(auto) visit(this Self&& self, Visitor&& vis);// 3

template<class R, class Self, class Visitor>
constexpr R visit(this Self&& self, Visitor&& vis);// 4
  • 1-2) Equivalent to the std::variant counterpart [spec], except that:

    •  — GET<m>(std::forward<V>(vars)) is replaced with unwrap_recursive(GET<m>(std::forward<V>(vars))).

  • 3-4) Equivalent to the std::variant counterpart [spec], except that it forwards to iris::visit instead of std::visit.

Hash support [rvariant.hash]

namespace std {

template<class... Ts>
struct hash<::iris::rvariant<Ts...>>;// 1

template<class T>
struct hash<::iris::recursive_wrapper<T>>;// 2

template<class T, class Allocator>
struct hash<::iris::recursive_wrapper_alloca<T, Allocator>>;// 3

} // std
namespace iris {

template<class... Ts>
/* constexpr */ std::size_t hash_value(rvariant<Ts...> const& v);// 1

template<class T>
/* constexpr */ std::size_t hash_value(recursive_wrapper<T> const& rw);// 2

template<class T, class Allocator>
/* constexpr */ std::size_t hash_value(recursive_wrapper_alloca<T, Allocator> const& rw);// 3

} // iris
  • 1) Effects: Equivalent to std::hash<rvariant<Ts...>>{}(v).

  • 2) Effects: Equivalent to std::hash<recursive_wrapper<T>>{}(rw).

  • 3) Effects: Equivalent to std::hash<recursive_wrapper_alloca<T, Allocator>>{}(rw).

I/O components are not included by the global convenience header (<iris/rvariant.hpp>).

operator<< support

// <iris/rvariant/rvariant_io.hpp>

namespace iris {

template<class T>
constexpr bool ADL-ostreamable = see below; // exposition only// 1

template<class... Ts>
std::ostream& operator<<(std::ostream& os, rvariant<Ts...> const& v);// 2

} // iris
  • 1) Evaluates to true if all of the following conditions are met; otherwise, evaluates to false.

    •  — Let os denote an lvalue reference to an object of type std::ostream, and let val denote an lvalue reference to an object of type T. These references are valid in unevaluated context, and

    •  — the expression os << val is well-formed and has the type std::ostream&, and

    •  — the corresponding overload is found solely via ADL.

  • 2) Constraints: ADL-ostreamable<unwrap_recursive_type<Ti>> is true for all i.

    Effects: Behaves as a formatted output function ([ostream.formatted.reqmts]) of os, except that:

    •  — the output is done as if by calling os << unwrap_recursive(GET<i>(v)) (with i being v.index()), and

    •  — any exception of type std::bad_variant_access, whether thrown directly (i.e., due to v being valueless) or indirectly (i.e., by a nested call to an alternative’s output function), is propagated without regard to the value of os.exceptions() and without turning on std::ios_base::badbit in the error state of os.

    Returns: os.

    Throws: std::bad_variant_access if v.valueless_by_exception() is true. Otherwise, throws any exception thrown as per the formatted output function’s specification.

std::formatter support

  • Let v denote an object of rvariant, and let proxy denote an object of variant_format_proxy.

  • The specialization std::formatter<::iris::rvariant<Ts...>, charT> (for arbitrary charT) is enabled if and only if std::formattable<unwrap_recursive_type<Tsi>, charT> is true for all i, with the following characteristics:

    •  — The format specifier must be empty, otherwise std::format_error is thrown, and

    •  — if v.valueless_by_exception() is true, std::bad_variant_access is thrown, and

    •  — the output is done as if by calling std::format_to(fmt_ctx.out(), paren, unwrap_recursive(GET<v.index()>(v))), with paren being a string literal "{}" interpreted on the target character type.

    • Example:

      std::println("{}", iris::rvariant<int, double>(42)); // prints 42
  • The specialization std::formatter<variant_format_proxy<VFormat, Variant>, charT> is enabled if and only if:

    •  — std::remove_cvref_t<VFormat> is a specialization of variant_format_string, and

    •  — std::remove_cvref_t<Variant> is a specialization of rvariant, and

    •  — std::formattable<unwrap_recursive_type<Tsi>, charT> is true for all i, with Ts being the template parameter pack of cv-unqualified non-reference type for Variant.

  • It has the following characteristics:

    •  — The format specifier must be empty, otherwise std::format_error is thrown, and

    •  — if v.valueless_by_exception() is true, std::bad_variant_access is thrown, and

    •  — the output is done as if by calling std::format_to(fmt_ctx.out(), proxy.v_fmt(std::in_place_type<Ts...[proxy.v.index()]>), unwrap_recursive(GET<proxy.v.index()>(proxy.v))), with Ts being the template parameter pack of the cv-unqualified non-reference type of proxy.v.

    • Example:

      using V = iris::rvariant<int, double>;
      constexpr auto v_fmt = iris::variant_format_for<V>("{:04d}", "{:.1f}");
      std::println("foo{}bar", iris::format_by(v_fmt, V(42)); // prints foo0042bar
      std::println("foo{}bar", iris::format_by(v_fmt, V(3.14)); // prints foo3.1bar
template<class... CharLike>
using select-char-t = see below; // exposition only
  • Denotes charT, with charT being the character type for which std::is_convertible_v<CharLikei, std::basic_string_view<charT>> is true for all i. If there exists no such substitution, the program is ill-formed.

// <iris/rvariant/rvariant_io.hpp>

template<class charT, class... Ts>
struct variant_format_string // exposition only
{
  std::basic_format_string<charT, Ts...[i] const&> fmts...;
  auto const& operator()(std::in_place_type<Ts...[i]>) const noexcept { return fmts...[i]; }
};

namespace iris {

template<class... Ts, class... Fmts>
constexpr variant_format_string<see below> variant_format(Fmts&&... fmts) noexcept;// 1

template<class Variant, class... Fmts>
constexpr variant_format_string<see below> variant_format_for(Fmts&&... fmts) noexcept;// 2

} // iris
  • 1) Mandates: std::is_convertible_v<Fmtsi, std::basic_format_string<select-char-t<Fmts...>, Tsi const&>> is true for all i, and sizeof...(Ts) > 0 is true.

    Let charT denote select-char-t<Fmts...>.

    Returns: variant_format_string<charT, Ts...>{std::forward<Fmts>(fmts)...}.

  • 2) Mandates: std::is_convertible_v<Fmtsi, std::basic_format_string<select-char-t<Fmts...>, Tsi const&>> is true for all i, with Ts being the template parameter pack of cv-unqualified non-reference type for Variant. Such substitution is valid only if std::remove_cvref_t<Variant> is a specialization of rvariant.

    Let charT denote select-char-t<Fmts...>.

    Returns: variant_format_string<charT, Ts...>{std::forward<Fmts>(fmts)...}.

// <iris/rvariant/rvariant_io.hpp>

template<class VFormat, class Variant>
struct variant_format_proxy // exposition only
{
    VFormat v_fmt;
    Variant v;
};

namespace iris {

template<class VFormat, class Variant>
constexpr variant_format_proxy<VFormat, Variant>
format_by(VFormat&& v_fmt, Variant&& v) noexcept;// 1

} // iris
  • 1) Constraints: std::remove_cvref_t<VFormat> is a specialization of variant_format_string, and std::remove_cvref_t<Variant> is a specialization of rvariant.

    Returns: variant_format_proxy<VFormat, Variant>{std::forward<VFormat>(v_fmt), std::forward<Variant>(v)}.

Class template recursive_wrapper [rvariant.recursive]

namespace iris {

template<class T>
class recursive_wrapper
{
  see below
};

} // iris

Class template recursive_wrapper behaves like recursive_wrapper_alloca instantiated with std::allocator<T>, except that the allocator-related member functions (that is, member functions that take allocator-specific arguments) are omitted. (Note: this is a QoL feature to discard the rarely used std::allocator<T> type parameter from diagnostic messages; see iris-cpp/iris#43.)

namespace iris {

template<class T, class Allocator = std::allocator<T>>
class recursive_wrapper_alloca
{
  // provides the same functionality as std::indirect, unless otherwise noted

  // [rvariant.recursive.ctor], constructors
  constexpr /* not explicit */ recursive_wrapper_alloca();

  template<class U = T>
  constexpr /* not explicit */ recursive_wrapper_alloca(U&& x);
};

// equivalent to the std::indirect counterpart
template<class Value>
  recursive_wrapper_alloca(Value) -> recursive_wrapper_alloca<Value>;

// equivalent to the std::indirect counterpart
template<class Allocator, class Value>
  recursive_wrapper_alloca(std::allocator_arg_t, Allocator, Value)
    -> recursive_wrapper_alloca<
      Value,
      typename std::allocator_traits<Allocator>::template rebind_alloc<Value>
    >;

} // iris
// <iris/rvariant/recursive_wrapper_pmr.hpp>

namespace iris::pmr {

template<class T>
using recursive_wrapper = ::iris::recursive_wrapper_alloca<
  T, std::pmr::polymorphic_allocator<T>
>;

} // iris::pmr

Unless otherwise noted, the class template recursive_wrapper_alloca and relevant components in the namespace scope provide same functionality and have equivalent requirements as std::indirect.

Warning
recursive_wrapper and recursive_wrapper_alloca are not type alias of std::indirect and do not publicly derive from it.
Note
Although std::indirect is a C++26 feature, recursive_wrapper and recursive_wrapper_alloca can be used in C++23.

Constructors

Effectively overrides only the ones listed below; rest are the same as std::indirect counterparts. [spec]

constexpr /* not explicit */ recursive_wrapper_alloca();// 1

template<class U = T>
constexpr /* not explicit */ recursive_wrapper_alloca(U&& u);// 2
  • 1) Equivalent to the std::indirect counterpart,[spec] except that it is not explicit.

  • 2) Constraints:

    •  — std::is_same_v<std::remove_cvref_t<U>, recursive_wrapper_alloca> is false, and

    •  — std::is_same_v<std::remove_cvref_t<U>, std::in_place_t> is false, and

    •  — std::is_default_constructible_v<Allocator> is true, and

    •  — std::is_convertible_v<U, T> is true.

      Note 1: This prevents recursive instantiation of std::is_constructible, even for recursive types, while preserving SFINAE-friendliness. This specification is technically viable only because the class template rvariant never uses std::is_convertible in any of its constructor overloads. As a result, the atomic constraints of rvariant and recursive_wrapper remain mutually exclusive. However, if a user-defined class depends on both std::is_constructible and std::is_convertible (for the same rvariant specialization), it may trigger recursive instantiation.

      Note 2: It is currently unknown whether the recursive instantiation scenario described in Note 1 can be technically avoided without depending on the fragile mutual-exclusiveness on std::is_constructible and std::is_convertible. If you are aware of any technical insights, please contact us at iris-cpp/iris.

    Effects: Equivalent to the std::indirect counterpart. [spec]

recursive_wrapper helper utilities [rvariant.recursive.helper]

namespace iris {

template<class T>
using unwrap_recursive_type = see below;

} // iris
namespace iris {

template<class T>
constexpr auto&& unwrap_recursive(T&& o) noexcept;

} // iris

Additional Information

Visitation Technique in Depth

Some readers might assume that table-based visitation is the ideal approach, implying that visitations have constant time complexity.

using overload_type = R(*)(Visitor&&, Storage&&);
constexpr overload_type vtable[] = {
  &do_visit<0, Visitor, Storage>,
  &do_visit<1, Visitor, Storage>,
  &do_visit<2, Visitor, Storage>,
  ...
};
vtable[v.index()](vis, v.storage()); // O(1), of course

Early std::variant implementations used this function-pointer-based dispatch, but it was later found that this pattern results in poor inlining in major compilers, whereas switch-case-based dispatch is significantly better optimized (link). GCC, LLVM, and MSVC subsequently adopted the latter approach.

Unfortunately, GCC enables the optimization only in limited scenarios (link), and LLVM has reverted it due to unresolved issues (link). Our benchmark results reflect this status quo, with rvariant performing up to about 2x faster than GCC/Clang.

License

This library is distributed under the MIT License.