<!DOCTYPE html>
<html lang="en"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<style>
pre {font-family: "Consolas", "Lucida Console", monospace; margin-left:20pt; }
code {font-family: "Consolas", "Lucida Console", monospace; }
pre > i   { font-family: "Consolas", "Lucida Console", monospace;  font-style:italic; }
code > i  { font-family: "Consolas", "Lucida Console", monospace;  font-style:italic; }
pre > em  { font-family: "Consolas", "Lucida Console", monospace;  font-style:italic; }
code > em { font-family: "Consolas", "Lucida Console", monospace;  font-style:italic; }
dl > dt { font-style:italic; }
body { font-family: "Calibri" }

@media (prefers-color-scheme: dark) {
	body { background: #111; color:  #ccc; }
	a { color:  #38f; }
	a:visited { color:  #a4d; }
	.sect { color:  #ccc; }
    del { text-decoration: line-through; color: #EE9999; }
    ins { text-decoration: underline; color: #99EE99; }
    blockquote.std    { color: #ccc; background-color: #2A2A2A;  border: 1px solid #3A3A3A;  padding-left: 0.5em; padding-right: 0.5em; }
    blockquote.stddel { text-decoration: line-through;  color: #ccc; background-color: #221820;  border: 1px solid #332228;  padding-left: 0.5em; padding-right: 0.5em; ; }
    blockquote.stdins { text-decoration: underline;  color: #ccc; background-color: #182220;  border: 1px solid #223328; padding: 0.5em; }
    table { border: 1px solid #ccc; border-spacing: 0px;  margin-left: auto; margin-right: auto; }
}

@media (prefers-color-scheme: light) {
	body { background:  white; color: black; }
    del { text-decoration: line-through; color: #8B0040; }
    ins { text-decoration: underline; color: #005100; }
    blockquote.std    { color: #000000; background-color: #F1F1F1;  border: 1px solid #D1D1D1;  padding-left: 0.5em; padding-right: 0.5em; }
    blockquote.stddel { text-decoration: line-through;  color: #000000; background-color: #FFEBFF;  border: 1px solid #ECD7EC;  padding-left: 0.5em; padding-right: 0.5em; ; }
    blockquote.stdins { text-decoration: underline;  color: #000000; background-color: #C8FFC8;  border: 1px solid #B3EBB3; padding: 0.5em; }
    table { border: 1px solid black; border-spacing: 1px;  margin-left: auto; margin-right: auto; }
}


.comment em { font-family: "Calibri"; font-style:italic; }
p.example   { margin-left: 2em; }
pre.example { margin-left: 2em; }
div.example { margin-left: 2em; }
div.poll { margin-left: 2em; }

code.extract { background-color: #F5F6A2; }
pre.extract  { margin-left: 2em; background-color: #F5F6A2;  border: 1px solid #E1E28E; }

p.function    { }
.attribute    { margin-left: 2em; }
.attribute dt { float: left; font-style: italic;  padding-right: 1ex; }
.attribute dd { margin-left: 0em; }

.editor { color: #4444BB; font-style: normal; background-color: #DDDDDD; }

tab { padding-left: 4em; }
tab3 { padding-left: 3em; }

.link { float: right; font-family: "Consolas", "Lucida Console", monospace; font-size:80% }


table.header { border: none; border-spacing: 0;  margin-left: 0px; font-style: normal; }
td.header { border: none; border-spacing: 0;  margin-left: 0px; font-style: normal; }
.header { border: none; border-spacing: 0;  margin-left: 0px; font-style: normal; }
table.poll { border: 1px solid black; border-spacing: 0px;  margin-left: 0px; font-style: normal; }

th { text-align: left; vertical-align: top;  padding-left: 0.4em;  /*padding-right: 0.4em; border-bottom:1px dashed;*/ }
td { text-align: left;  padding-left: 0.4em; padding-right: 0.4em; /*border-right:1px dashed; */}
tr { border: solid; border-width: 1px 0; border-bottom:1px solid blue }


.revision   { /*color: #005599;*/ }
.grammar { list-style-type:none }

</style>

<title>Range interface in std::optional breaks code!</title>

</head>
<body>  

<table class="header"><tbody>
  <tr>
    <th>Document number:&nbsp;&nbsp;</th><th> </th><td class="header">P3415R0</td>
  </tr>
  <tr>
    <th>Date:&nbsp;&nbsp;</th><th> </th><td class="header">2024-10-10</td>
  </tr>
  <tr>
    <th>Audience:&nbsp;&nbsp;</th><th> </th><td class="header">LEWG</td>
  </tr>
  <tr>
    <th>Reply-to:&nbsp;&nbsp;</th><th> </th><td class="header">
        <address>Andrzej Krzemieński &lt;akrzemi1 at gmail dot com&gt;</address>
    </td>
  </tr>
</tbody></table>



<h1>Range interface in <code>std::optional</code> breaks code!</h1>


<p> <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p1255r12.pdf"
       title="Steve Downey, &ldquo;A view of 0 or 1 elements: views::maybe&rdquo;">[P1255R12]</a>
    provided a motivation for treating optional objects as ranges of zero or one elements
    in certain contexts, and proposed a solution for <em>opting into</em> such treatment
    via a dedicated adapter. 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html"
       title="Marco Foco, Darius Neațu, Barry Revzin, David Sankel, &ldquo;Give std::optional Range Support&rdquo;">[P3168R2]</a>
    went further and proposed <em>forcing</em> the treatment of optional objects as ranges,
    even in places where this is not desired, without offering a strong enough motivation.
    In this paper we argue that this "improvement" is harmful, and that it will break 
    existing libraries and user code. We propose to revert changes in 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html"
       title="Marco Foco, Darius Neațu, Barry Revzin, David Sankel, &ldquo;Give std::optional Range Support&rdquo;">[P3168R2]</a>
    and to reconsider 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p1255r12.pdf"
       title="Steve Downey, &ldquo;A view of 0 or 1 elements: views::maybe&rdquo;">[P1255R12]</a>,
    which is not intrusive.       
    </p>


<h2><a class="sect" id="1">1.</a> The model behind <code>std::optional</code></h2>

<p> I am one of the authors of 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3793.html"
       title="Fernando Cacciola, Andrzej Krzemieński, &ldquo;A proposal to add a utility class to represent optional objects (Revision 5)&rdquo;">[N3793]</a>,
    author of the 
    <a href="https://github.com/akrzemi1/Optional">reference implementation</a>
    of <code>std::optional</code> and a maintainer of <a href="https://www.boost.org/doc/libs/1_86_0/libs/optional/doc/html/index.html">Boost.Optional</a>
    library. This paper is based on experience with 
    a real library servicing many customers, private and commercial. 
    After the years of experience with Boost.Optional and <code>std::optional</code>, 
    I conclude that its design is broken. The main reason, as Tony Van Eerd observed years ago,
    is that it does not have a one clear answer to the question what it is. Sometimes it 
    is a container of <code>T</code>, sometimes it is just <code>T</code> with an
    additional value <code>nullopt</code>. These two models can coexist to some degree, 
    but at some point they clash and <code>std::optional</code> will surprise the user.
    For instance, one can expect of the container, as a safety feature, that a container
    cannot be compared to its element:
    </p>
    
<pre>
return std::vector{1} == 1; <em>// will not compile</em>
</pre>

<p> Even though we could provide quite intuitive semantics to such comparison (size of 1
    and the contained element equals the value), we do not do it, in order to protect
    the user from accidental unintended usages of such comparison. However, 
    <code>std::optional</code> &mdash; considered a container by some &mdash; does offer 
    such mixed comparison, sometimes causing harmful effects: 
    </p>
    
<pre>
optional&lt;double&gt; Flight_plan::weight(); 
  <em>// `nullopt` when we cannot compute weight</em>

bool is_aircraft_too_heavy(Flight_plan const&amp; p)
{
  double max_weight = p.aircraft().max_weight();
  return p.weight() &gt; max_weight; <em>// compiles!</em>
}                                 <em>// returns `false` on `nullopt`</em>                                              
</pre>
    
<p> We cannot fix this, as this would be a breaking change, but what we can do is to 
    avoid making this rift between different models bigger. We should not pretend
    that <code>std::optional</code> is a container and add a range interface to it,
    while at the same time pursuing optional references 
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2988r7.pdf"
       title="Steve Downey, Peter Sommerlad, &ldquo;std::optional&lt;T&amp;&gt;&rdquo;">[P2988R7]</a>).
    No container supports references. If a component is just a bag of features without a 
    clear model, people cannot predict how it will interact with other features.
    </p>

<p> <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html"
       title="Marco Foco, Darius Neațu, Barry Revzin, David Sankel, &ldquo;Give std::optional Range Support&rdquo;">[P3168R2]</a>
    gives an impression that the trade-off is between adding a new adapter type and not
    adding a new type. But a more accurate description would be that the trade-off 
    is between adding a new adapter type and compromising the integrity of an existing type,
    and breaking the currently working code, as demonstrated in the next section.
    </p>



<h2><a class="sect" id="2">2.</a> Breaking libraries and user code</h2>


<p> The present C++ Standard Library provides concepts like 
    <code>std::ranges::range</code>, therewith encouraging and endorsing the SFINAE-style
    detection of the range interface and making some compile-time decisions.
    It is reasonable to assume that people already check
    if user types model <code>std::ranges::range</code> and select different overloads.
    Any such code may automatically break when C++ suddenly makes <code>std::optional</code>
    a range.
    </p>
    
<p> Interestingly, one such undesired effect was discovered in the Standard Library itself,
    where <code>optional&lt;int&gt;</code> would become formattable as a range. So the
    range formatting protocol has been explicitly disabled for 
    <code>optional&lt;int&gt;</code>. The Standard Library can afford to disable it 
    for its own usages at the same time as introducing the change. But other libraries
    cannot.
    </p>

<p> As an evidence of this breakage, consider library 
    <a href"https://www.boost.org/doc/libs/1_86_0/libs/json/doc/html/index.html">Boost.JSON</a>.
    It offers a way to convert a user type to a JSON "value". This is documented 
    <a href="https://www.boost.org/doc/libs/1_86_0/libs/json/doc/html/json/conversion.html">here</a>.
    A JSON value is one of a couple of categories (string, number, integer, boolean, array,
    object), and each converted user-defined type needs to be assigned one of these categories.
    This assignment is based on the overload resolution. Being a range renders category "array"
    and is prioritized over being optional-like, which renders category "object".    
    </p>
    
<pre>
std::optional&lt;int&gt; o1 = 16;
json::value v1 = json::value_from(o1);             <em>// (1) store optional as JSON value (integer)</em>
std::string s = json::serialize(v1);               <em>// (2) serialize to text: `16`</em>
json::value v2 = json::parse(s);                   <em>// (3) parse text to JSON value (integer)</em>
auto o2 = json::value_to&lt;std::optional&lt;int&gt;&gt;(v2);  <em>// (4) convert JSON value integer to optional</em>
</pre>

<p> This illustrates the use case where we want to serialize our data into JSON text
    representation, and then recreate our object on the other side from the JSON text
    representation. Function <code>json::value_from</code> is overloaded. First,
    an overload is considered that tests the type predicate 
    <code>is_sequence_like&lt;T&gt;</code>,
    which fails, and then the type predicate 
    <code>is_optional_like&lt;T&gt;</code>, which succeeds. This means that 
    <code>std::optional&lt;int&gt;</code> is considered a JSON integer, and printed as 
    string <code>`16`</code>.</p>
    
<p> If <code>std::optional</code> becomes a range one day, the categorization above will
    change. Type trait <code>is_sequence_like&lt;T&gt;</code> will return <code>true</code>,
    and <code>std::optional&lt;int&gt;</code> will be treated as a JSON array, producing
    text output <code>`[16]`</code>. This is a breaking change of the worst kind:
    the program still compiles but has different semantics. The different
    JSON output may no longer be schema-compliant, and break its specification. 
    It will be discovered late in the game, because compilation succeeds, initial tests 
    may pass. The tickets will be filed against 
    <a href"https://www.boost.org/doc/libs/1_86_0/libs/json/doc/html/index.html">Boost.JSON</a>
    library rather than the C++ Standard.
    </p>
    
<p> There is a second breaking change here. If this new value is
    read back into <code>std::optional&lt;int&gt;</code>, function <code>json::value_to</code>
    using the same trait <code>is_sequence_like&lt;T&gt;</code>, will select the 
    <em>sequence protocol</em> for parsing the array and call:
    </p>
    
<pre>
o2.insert(value, o2.end());
</pre> 

<p> And this will now fail to compile. This will break the existing programs that use 
    <a href"https://www.boost.org/doc/libs/1_86_0/libs/json/doc/html/index.html">Boost.JSON</a>
    today.  
    </p>
    
<p> Here is a full example in Compiler Explorer: 
    <a href="https://godbolt.org/z/5YW36bcex">https://godbolt.org/z/5YW36bcex</a>.
    </p>

<p> The fix to this breakage, for <a href"https://www.boost.org/doc/libs/1_86_0/libs/json/doc/html/index.html">Boost.JSON</a>,
    is to special-case the type trait <code>is_sequence_like&lt;T&gt;</code>
    so that it detects all
    range-like types <em>except for</em> <code>std::optional&lt;T&gt;</code>.
    This repeats the same pattern as the fix for <code>std::format</code>:
    after <code>std::optional&lt;T&gt;</code> is made a range, libraries have to explicitly 
    request that it should not be treated as a range. It is somewhat ironic that while
    the goal of
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html"
       title="Marco Foco, Darius Neațu, Barry Revzin, David Sankel, &ldquo;Give std::optional Range Support&rdquo;">[P3168R2]</a>
    is to make <code>std::optional&lt;T&gt;</code> model concepts like 
    <code>std::ranges::range</code>, the libraries negatively impacted by it, will have to
    now devise a new concept, "any range but optional objects".
    </p>




<h2><a class="sect" id="3">3.</a> Profit and loss balance</h2>



<p> How does this cost weigh against the benefits?
    The primary motivation in 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p1255r12.pdf"
       title="Steve Downey, &ldquo;A view of 0 or 1 elements: views::maybe&rdquo;">[P1255R12]</a>
    is to have additional building blocks for ranges. There is a secondary motivation:
    the usage of <code>for</code>-loops instead of <code>if</code>-statements, but we 
    consider it a stretch. Besides, pattern matching 
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2688r2.html"
       title="Michael Park, &ldquo;Pattern Matching: match Expression&rdquo;">[P2688R2]</a>)
    will serve this purpose better. Thus, usages other than in ranges will at best be
    unaffected and at worst be damaged.
    </p>

<p> Users who compose range pipelines are already used to drawing components from the
    <code>ranges</code> namespace, and it is clear that the single purpose of these views
    is to be a building block in range pipelines. Adding building blocks in 
    <code>ranges</code> seems natural and intuitive, even if the implementation is similar
    to other components.</p>
    
<p> Should we care about breaking user code? 
    </p>
    
<p> <a href="https://isocpp.org/std/standing-documents/sd-8-standard-library-compatibility"
       title="Bryce Lelbach, &ldquo;Standard Library Compatibility&rdquo;">[SD-8]</a> 
    argues that if we avoided <em>any</em> potential breaking changes too literally,
    we wouldn't be able to add anything to the Standard Library, because any change can
    be detected via SFINAE tricks and, in the future, reflection. It lists specifically 
    that the Standard Library reserves the right to add members to the reserved namespaces
    and to classes in these namespaces.    
    </p>

<p> However, we claim that the situation in question is special and requires an individual
    approach. This is because the Standard library offers the concept 
    <code>std::ranges::range</code>. With this, the Standard Library recognizes that
    members, or hidden friends, <code>begin()</code> and <code>end()</code> have special
    meaning for the Standard Library. It recognizes that the user code will be testing for
    the presence of these members. <code>std::ranges::range</code> encourages testing
    for these functions. The change of the Standard Library component to start modeling 
    a Standard Library concept is a <strong>big</strong> change.
    </p>


<h2><a class="sect" id="4">4.</a> The proposal</h2>



<p> We propose to revert the changes applied by 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html"
       title="Marco Foco, Darius Neațu, Barry Revzin, David Sankel, &ldquo;Give std::optional Range Support&rdquo;">[P3168R2]</a>.
    </p>
    
<p> Whether 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p1255r12.pdf"
       title="Steve Downey, &ldquo;A view of 0 or 1 elements: views::maybe&rdquo;">[P1255R12]</a>
    should be added instead, is beyond the scope of this proposal. 
    </p>

<h2><a class="sect" id="5">5.</a> Proposed wording</h2>


<p> The proposed wording is relative to 
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/n4988.pdf"
       title="Thomas Köppe, &ldquo;Working Draft Programming Languages — C++&rdquo;">[N4988]</a>.
    </p>
    
<p> Remove declarations from <a href="https://eel.is/c++draft/optional.syn">[optional.syn]</a>
    as follows.</p>
    
<blockquote class="std">
<pre>namespace std {
  <em>// [optional.optional], class template optional</em>
  template&lt;class T&gt;
    class optional;                                     <em>// partially freestanding</em>
  
  <del>template&lt;class T&gt;</del>
  <del>  constexpr bool ranges::enable_view&lt;optional&lt;T&gt;&gt; = true;</del>   
  <del>template&lt;class T&gt;</del>
  <del>  constexpr auto format_kind&lt;optional&lt;T&gt;&gt; = range_format::disabled;</del>

  template&lt;class T&gt;
    concept is-derived-from-optional = requires(const T&amp; t) {  <em>// exposition only</em>
        []&lt;class U&gt;(const optional&lt;U&gt;&amp;){ }(t);
    };
  // ...
}</pre>
</blockquote>


<p> Remove declarations from 
    <a href="https://eel.is/c++draft/optional.optional.general">[optional.optional.general]</a>
    as follows.
    </p>
 
<blockquote class="std">
<pre>
namespace std {
  template&lt;class T&gt;
  class optional {
  public:
    using value_type             = T;
    <del>using iterator               = implementation-defined; <em>// see [optional.iterators]</em></del>
    <del>using const_iterator         = implementation-defined; <em>// see [optional.iterators]</em></del>

    <em>// ...</em>

    <em>// [optional.swap], swap</em>
    constexpr void swap(optional&amp;) noexcept(<em>see below</em>);
  
    <del><em>// [optional.iterators], iterator support</em></del>
    <del>constexpr iterator begin() noexcept;</del>
    <del>constexpr const_iterator begin() const noexcept;</del>
    <del>constexpr iterator end() noexcept;</del>
    <del>constexpr const_iterator end() const noexcept;</del>   
    
    <em>// [optional.observe], observers</em>
    constexpr const T* operator-&gt;() const noexcept;
    <em>// ...</em>
  };
}
</pre>
</blockquote>

<p> Remove clause 
    <a href="https://eel.is/c++draft/optional#iterators">[optional.iterators]</a>.
    </p>
    
<p> Remove a macro definition from 
    <a href="https://eel.is/c++draft/version.syn">[version.syn]</a>,
    header <code>&lt;version&gt;</code> synopsis, as follows.</p>

<blockquote class="std">
<pre>
    #define __cpp_lib_optional                   202110L <em>// also in &lt;optional&gt;</em>
    <del>#define __cpp_lib_optional_range_support     202406L <em>// freestanding, also in &lt;optional&gt;</em></del>
    #define __cpp_lib_out_ptr                    202106L <em>// also in &lt;memory&gt;</em>
</pre>
</blockquote>
 
 
 
<h2><a class="sect" id="6">6.</a> Acknowledgments</h2>

<p> Barry Revzin, Tim Song and Jens Maurer offered useful suggestions that increased the quality of the proposal. </p>


<h2><a class="sect" id="7">7.</a> References</h2>


<ul>

<li>[N3793] — Fernando Cacciola, Andrzej Krzemieński, "A proposal to add a utility class to represent optional objects (Revision 5)" <br>
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3793.html">https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3793.html</a>).
    </li>
    <!--
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3793.html"
       title="Fernando Cacciola, Andrzej Krzemieński, &ldquo;A proposal to add a utility class to represent optional objects (Revision 5)&rdquo;">[N3793]</a>
    -->	
    
<li>[P1255R12] — Steve Downey, "A view of 0 or 1 elements: <code>views::maybe</code>" <br>
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p1255r12.pdf">https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p1255r12.pdf</a>).
    </li>
    <!--
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p1255r12.pdf"
       title="Steve Downey, &ldquo;A view of 0 or 1 elements: views::maybe&rdquo;">[P1255R12]</a>
    -->	
    
<li>[P2688R2] — Michael Park, "Pattern Matching: <code>match</code> Expression" <br>
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2688r2.html">https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2688r2.html</a>).
    </li>
    <!--
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2688r2.html"
       title="Michael Park, &ldquo;Pattern Matching: match Expression&rdquo;">[P2688R2]</a>
    -->	     
    
<li>[P2988R7] — Steve Downey, Peter Sommerlad,
    "std::optional&lt;T&amp;&gt;", <br>
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2988r7.pdf">"https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2988r7.pdf"</a>).
    </li>
    <!--
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2988r7.pdf"
       title="Steve Downey, Peter Sommerlad, &ldquo;std::optional&lt;T&amp;&gt;&rdquo;">[P2988R7]</a>
    --> 
    
<li>[P3168R2] — Marco Foco, Darius Neațu, Barry Revzin, David Sankel,
    "Give <em>std::optional</em> Range Support", <br>
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html">"https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html"</a>).
    </li>
    <!--
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3168r2.html"
       title="Marco Foco, Darius Neațu, Barry Revzin, David Sankel, &ldquo;Give std::optional Range Support&rdquo;">[P3168R2]</a>
    --> 
    
<li>[SD-8] — Bryce Lelbach,
    "Standard Library Compatibility", <br>
    (<a href="https://isocpp.org/std/standing-documents/sd-8-standard-library-compatibility">"https://isocpp.org/std/standing-documents/sd-8-standard-library-compatibility"</a>).
    </li>
    <!--
    <a href="https://isocpp.org/std/standing-documents/sd-8-standard-library-compatibility"
       title="Bryce Lelbach, &ldquo;Standard Library Compatibility&rdquo;">[SD-8]</a>
    -->

<li>[N4988] — Thomas Köppe, "Working Draft Programming Languages — C++" <br>
    (<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/n4988.pdf">https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/n4988.pdf</a>).
    </li>
    <!--
    <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/n4988.pdf"
       title="Thomas Köppe, &ldquo;Working Draft Programming Languages — C++&rdquo;">[N4988]</a>
    -->	  

</ul>



</body></html>
