How my papers did at Kona

I’m now back home (sweet home) in New York after last week’s WG21 meeting in Kona, Hawaii. I’d arrived with three papers in the pre-Kona mailing, and a few more in flight from earlier. Here’s a status report on those papers, and some others.

P1144R9 is_trivially_relocatable

No movement. There continue to be two essentially competing proposal directions: my own P1144 (Godbolt) and Bloomberg’s P2786 / P2959 / P2967 (cf. P2685, P2839). P1144 continues its Pyrrhic dominance of the implementation space, but since 2022 has been losing hard in the paper space.

For the areas of contention between P1144 and the Bloomberg proposals, see my post-Issaquah blog series (I, II, III, IV).

Bloomberg’s P2786 was seen in an EWG Friday session, which lacked quorum and was thus merely “informational.”

The main point of this discussion was the wacky behavior of std::vector<std::pmr::string> — although we used tuple<int&> as a stand-in for pmr::string. Alisdair Meredith points out in P2959 that node-based containers behave predictably for non-regular types like tuple<int&> and pmr::string, but non-node-based containers like vector and deque (and perhaps one day hive) have uniquely unpredictable behavior. Godbolt:

int data[10] = {0,1,2,3,4,5,6,7,8,9};
std::deque<std::tuple<int&>> d = { {data[0]} };
for (int i=1; i < 10; ++i) {
    d.emplace(d.begin() + 1, data[i]);
}

After this loop, the contents of d are more or less arbitrary — as are the contents of data itself! Whenever deque internally move-constructs one of its elements of type tuple<int&>, that’s essentially a pointer copy; whenever deque internally move-assigns an element, that’s a write-through modification of some element of data. The output depends not only on the deque vendor’s algorithmic choices (construct, assign, swap) but also on the deque’s block size. For the above example, libstdc++ ends up with 9,3,2,3,4,5,6,7,8,9 in data; libc++ gives 9,2,2,3,4,5,6,7,8,9; Microsoft STL gives 2,1,3,4,5,6,7,8,9,0.

Alisdair’s P2959 “Relocation within containers,” if I understand correctly, basically asks for deque to “act predictable” here and forcibly treat tuple<int&> as if it were a regular type (i.e. copy its object representations around, rather than ever delegate that job to a user-provided tuple<int&>::operator= that might Do The Wrong Thing). This being C++, we’d hide that “forcibly” part behind a couple layers of abstraction — ultimately it would be something like allocator<tuple<int&>>::relocate making the decision — but for ordinary programmers that would all be hidden away.

Less ambitiously, we could at least stop forcing deque to call tuple<int&>::operator=, and loosen the wording to permit deque (if it likes) to assume that its element type is regular. The library wording boils down to rewriting [vector.modifiers] and [deque.modifiers] to change their mentions of “assignment” to something more handwavey, like “alteration of value,” leaving the exact mechanisms of that alteration up to the vendor.

P1144 has no particular horse in that race. I think it would be great to change [vector.modifiers] and [deque.modifiers] as suggested in the previous paragraph — I might even bring such a paper in the December post-Kona mailing — but from P1144’s point of view, tuple<int&> is simply not trivially relocatable (due to its user-defined operator=). If you create a vector<tuple<int&>> on your hot path then maybe bad performance is what you deserve. Want something that optimizes well? Use a regular type, such as int* or reference_wrapper<int> or tuple<int*> (all three of which are trivially relocatable: Godbolt).

UPDATE, January 2024: Reader, I did bring that paper.
Previously: “P1144 PMR koans” (2023-06-03).
Subsequently: “Should assignment affect is_trivially_relocatable?” (2024-01-02).

During the meeting, I submitted to Charles Salvia’s GitHub repository implementing P0709 “Zero-overhead deterministic exceptions” (“Herbceptions”) a patch enabling P1144 library support, so that it now supports using any (pointer-sized, copyable) trivially relocatable type as its payload type. (Godbolt.)

I’m also pleased to report that the Stellar HPX implementation, Phase 1 of which Isidoros Tsaousis completed during this year’s Google Summer of Code, continues to make progress.

P2447R5 span over initializer list

Adopted for C++26! This means that starting in C++26, you’ll be able to write:

void oldf(const std::vector<int>&);
void newf(std::span<const int>);

int main() {
    oldf({1,2,3,4}); // still works
    newf({1,2,3,4}); // newly works!
}

Thanks to P2752 “Static storage for braced initializers” (adopted as a DR in Varna, further improved by CWG2753 this week), the newf line will avoid not only the heap-allocation of vector, but even any stack-allocation for the initializer_list’s backing array. This is a great boon for both the reputation of the modern language (as more users are now able to switch frictionlessly from const vector& to span) and for performance (as the span-taking version has become more performant).

Further improving the drop-in-replaceability of span for const vector&, C++26 also adopted P2821 “span.at().

P2767R2 flat_map/flat_set omnibus

No movement.

P2848R0 is_uniqued

No movement.

P2952R0 auto& operator=(X&&) = default

(That is, a proposal to support auto placeholders in the return types of defaulted special member functions. The compiler knows what type to put there; it’s just a quirk of the wording that you have to spell it out.)

No movement.

P2953R0 Forbid defaulting operator=(X) &&

(That is, if you claim your operator= is callable only on rvalues and never on lvalues, the compiler should disclaim all notion of what its “default” implementation might look like, and force you to write out your idea by hand.)

No movement.

P3016R0 Inconsistencies in begin/end for valarray and initializer_list

The core-language part of this paper addressed a tiny wording defect in the treatment of

for (int i : {1,2,3,4}) ~~~~

(namely, that the for-range-initializer here is not syntactically an expression, so the wording didn’t mean what GCC, Clang, and EDG all thought it meant). This turned into CWG2825, and will likely be favorably resolved soon. The library parts of P3016R0 went unseen, but at least now P3016R1 will affect only the library. Because of the Committee’s structure, it’s vastly easier to move a paper that affects “only library” or “only core” than to move a paper that affects both simultaneously.

P3031R0 Conversion function for explicit-object lambda

This was a fun one. New in C++23, the idea of an explicit-object member function is that instead of taking its this argument implicitly (as an implicit object parameter), it takes it explicitly as an explicit object parameter. (This is unrelated to the explicit keyword.) So you can now write C++ that “looks like Python” in a bad way:

struct S {
    int x_ = 42;

    int oldf(int y) const { return x_ + y; }
    int newf(this const S& self, int y) { return self.x_ + y; }
};

Of course you don’t need to write newf like that; explicit-object syntax isn’t “the new way to write functions,” any more than std::ranges is “the new way to write algorithms.” But explicit-object syntax lets you do a few cool things. For example, you can do the CRTP without templates! (Godbolt.)

struct DoubleSpeaker {
    template<class Derived>
    void speak_twice(this Derived& self) {
        self.speak();
        self.speak();
    }
};
struct Mouse : DoubleSpeaker {
    void speak() { puts("squeak"); }
};
struct Lion : DoubleSpeaker {
    void speak() { puts("ROAR"); }
};
int main() {
    Mouse m; m.speak_twice();
    Lion n; n.speak_twice();
}

And you can make recursive lambdas! (Godbolt.)

auto fib = [](this auto fib, int x) -> int {
    return x < 2 ? x : fib(x-1) + fib(x-2);
};
static_assert(fib(10) == 55);

But those recursive lambdas are weird. With a normal captureless lambda, I can do +f to convert it implicitly to a function pointer. What happens if I do +fib here? Clang and MSVC both implemented this conversion operation, but both essentially by accident, without really thinking about what its semantics should be (and it turns out that the formal wording didn’t clarify matters). So Clang just returned &fib::operator(), whereas MSVC returned something like the equivalent of +[](int x) { return decltype(fib)()(x); }. The former is Clearly Just Wrong. The latter is temptingly useful, but produces very unintuitive behavior if inheriting from a lambda type is allowed to inherit its conversion-to-function-pointer operator. You can end up in situations where x() and (+x)() do different things.

It was perspicaciously noted that captureful lambdas do not convert to function pointers precisely because they need the lambda object’s data in order to work; and explicit-object lambdas (certainly this auto lambdas, but also this std::any lambdas) also basically ask for a reference to the lambda object’s data. So it seems consistent for C++23 to treat explicit-object lambdas the same in this respect as C++11 treats captureful lambdas.

The C++23 working draft no longer implies that explicit-object lambdas may be implicitly converted to function pointers. That doesn’t mean you couldn’t write a proposal to add that feature back — in fact I’d be happy to advise on such a proposal! But as this issue showed, it’s a very knotty problem that likely doesn’t have a simple solution.

Meanwhile, there is a simple workaround if you ever do actually need to turn fib into a function pointer:

auto fib = [](int x) {
    return [](this auto fib, int x) -> int {
        return x < 2 ? x : fib(x-1) + fib(x-2);
    }(x);
};
auto fptr = +fib; // OK

Other papers

I was pleased to see Gor Nishanov’s P2927 “Inspecting exception_ptr (a.k.a. “try_cast”) make progress in LEWG. The basic idea here is to permit “throwing” and “catching” an exception without invoking any of the runtime’s invisible-control-flow machinery. Instead of:

try {
    throw std::logic_error("hi");
} catch (const std::runtime_error& ex) {
    printf("RE %s\n", ex.what());
} catch (const std::exception& ex) {
    printf("EX %s\n", ex.what());
} catch (...) {
    printf("otherwise\n");
}

you can instead write:

std::exception_ptr e = std::make_exception_ptr(std::logic_error("hi"));
if (auto *ex = std::try_cast<std::runtime_error>(e)) {
    printf("RE %s\n", ex->what());
} else if (auto *ex = std::try_cast<std::exception>(e)) {
    printf("EX %s\n", ex->what());
} else if (e != nullptr) {
    printf("otherwise\n");
}

make_exception_ptr has been there since C++11; the novel part is try_cast. That function’s signature is

template<class E>
const E* try_cast(const std::exception_ptr&);

and it acts more or less like catch (const E& ex) { return &ex; }, except that the exception remains in-flight (still owned by the caller’s original exception_ptr), so that the returned pointer doesn’t dangle. If a catch handler of type const E& wouldn’t be hit, then try_cast<E> returns nullptr.

Would std::exception_cast be a better name? All exception_ptr’s other operations involve the word exception; e.g. std::rethrow_exception(e). On the other hand, the cast operation requires the caller to name an exception type inside the angle brackets, which seems pretty unambiguous. Do you prefer std::try_cast<std::runtime_error>(e) or std::exception_cast<std::runtime_error>(e)? People with strong opinions should email me and/or Gor with their comments.

I came to Kona thinking that the right design for try_cast would be to basically take the type in the angle brackets and copy-paste it into the catch handler, so that try_cast<int&> would act like catch (int& ex) and try_cast<int()> would act like catch (int ex()) (which decays to int (*ex)() just like in any other parameter list). But I was soon convinced that the simpler, stricter signature above was far superior — and that’s exactly what P2927’s next revision will propose.

There’s many things you can copy-paste to produce a syntactically valid catch handler, but many of them are subtle or flat-out nonsensical. For example, we’ve already seen that you can technically write catch (int()) to catch a function pointer, or catch (int[]) to catch an int*. Worse, catch (int(&)()) is syntactically valid but produces an unreachable handler, because only objects can be thrown, and a function reference can never bind to an object. (This is GCC bug 69372.) Also, it’s legal to catch (int&) but not (int&&). The simpler, stricter signature (we additionally mandate that E be a complete non-array object type, and not a pointer to an incomplete type either) avoids all of this nonsense and provides “just the functionality, ma’am.”

“But it returns const E* — what if I want to modify or augment the exception object in flight?” No problem! Just const_cast that pointer. It’s 100% safe; the underlying exception object is never const. The ugliness and greppability of const_cast are features, not bugs.


Peter Sommerlad’s P2968 “Make std::ignore a first-class object” didn’t move much in Kona, but I think it’s in good shape and on track to make C++26. This small paper provides the Standard’s blessing to code like

std::tie(std::ignore) = std::make_tuple(42); // OK
std::ignore = 42;                            // UB

which is technically UB today: std::ignore’s behavior is well-specified by the paper standard only when it’s wrapped inside a tuple. Of course it works fine in real life; the paper standard just hasn’t been saying so. Peter fixes that, and also addresses some minor implementation divergence in libc++ along the way.

template<template<class> class C, class T>
  constexpr bool f(C<T>) { return false; }
template<class T>
  constexpr bool f(T) { return true; }
static_assert(f(std::ignore));
    // libc++ fails

struct S { int i : 2; } s;
void f() { std::ignore = s.i; }
    // libc++ fails

Brian Bi’s P2748 “Disallow binding a returned glvalue to a temporary” almost made C++26 this meeting, but needs some minor tweaks to its wording. It patches a corner case in the ongoing saga of return x. C++23’s simpler implicit move successfully disallowed dangerously dangling returns like

int& f(int i) { return i; } // OK in '20, error in '23+'26

while naturally continuing to permit

int& f(int& i) { return i; } // always OK
int&& f(int&& i) { return i; } // error in '20, OK in '23+'26

After Brian’s paper, C++26 will additionally disallow

long&& f(int&& i) { return i; } // OK in '20+'23, error in '26

where the trick is that the returned long&& isn’t binding to i (the int object from f’s caller’s scope) — it’s materializing temporary long from the value of i, and binding to that temporary! Directly returning a reference bound to a temporary in this way is no longer permitted; the warning your compiler already gives on this line will in C++26 become an error.

Now the bad news: even after Brian’s paper, C++26 continues to permit you to return a reference bound to an automatic variable, even a move-eligible variable. For example, this return is still legal C++26, although of course every vendor warns about it:

const int& f(int i) { return i; } // always OK (oops)

Type-system-based local reasoning will never stamp out all dangling. The following will always be legal C++:

int& g(int& r) { return r; } // safe in isolation
int& f(int i) { return g(i); } // dangles in combination

Brian also brought a paper P2991 “Stop forcing std::move to pessimize” which got a relatively (not landslidingly) negative reception. Rightly so, in my opinion, since that paper basically asked us to encourage more use of return std::move(x) just as the language is finally finding solid footing with return x!


One paper I paid no attention to, but which is now adopted for C++26 and looks important, is P2662 “Pack Indexing” (Jabot & Halpern, 2023). In C++26 you’ll be able to write a variadic cadr as

auto cadr(auto... ts) { return ts...[1]; }

Another important paper adopted for C++26 is P1673 “A linear-algebra interface based on the BLAS” (13 authors). It adds a lot of functions, types, and concepts to the Standard, all isolated within a new std::linalg namespace.

Posted 2023-11-12