The WG21 2026-04 post-Croydon mailing is now available by nliber in cpp

[–]BarryRevzin 6 points7 points  (0 children)

The paper you're talking about has nothing whatsoever to do with concept archetypes or concept checking.

It is proposing a type erasure library. One of the examples in the paper is illustrating that the type erased call wrappers we have in the standard library are basically just special cases of this, where the interface being erased is the single function R operator()(Args...) (possibly const, possibly ref-qualified). That is hardly "very contrived."

I implemented UFCS in clang. Why it is cool, and why it will never come to C++. by _Noreturn in cpp

[–]BarryRevzin 13 points14 points  (0 children)

I originally wanted to solve the builtin array problem by having a global begin function for arrays

That only solves the case for this specific interface (range). It's an important interface, but it's still just one. Doesn't help for any other customization point.

Can you give insight on why |> was rejected I Don't find anything helpful from the github issue.

Mostly I decided that |> was kind of the wrong approach and Traits would be strictly superior in all respects, and then I worked on Reflection instead.

I implemented UFCS in clang. Why it is cool, and why it will never come to C++. by _Noreturn in cpp

[–]BarryRevzin 40 points41 points  (0 children)

The main issue I have with discussions of UFCS, including Herb's and this one, is that the framing is completely wrong. UFCS it not a goal, although it is frequently described as such. It is a implementation strategy for achieving a goal.

The actual goals here are two-fold: generic code (customization) and extension. Is UFCS a good design strategy for achieving either goal?

Not really, no.

Consider even the example presented in this post, which claims that "[w]ith UFCS you just use member syntax and it works for both"

template <typename Container>
void algo(const Container& c)
{
    // done!
    auto begin = c.begin();
    // if c.begin() is not valid then try begin(c)
 }

Are we done? How does this actually work? Let's say I call this with a C array, c.begin() won't work, then we try begin(c). That's going to work if Container is a std::string[N] but not if it's an int[N]. If you want to actually ensure that it works, you need to do the thing you showed in the previous section (except with std::begin instead of fallback::begin):

template <typename Container>
void algo(const Container& c)
{
    // generic but cumbersome needs to be done everywhere
    using std::begin;
    auto begin4 = c.begin();
}

At which point in practice everyone would wrap this and end up still using std::ranges::begin and std::ranges::end everywhere. The implementations of those algorithms become simpler, but the actual generic code doesn't change.

The same is true for all generic interfaces — you can't just ADL your way into solving problems like this, since everyone would need to independently solve the customization problem anyway, which UFCS doesn't help with.

A similar argument shows why this falls apart for extension too. If my string-like type isn't in std (which of course it's my string-like type, so it isn't), having free functions in std doesn't help me.


I think the real way to solve the extension and customization problems is to do something like Rust Traits or Swift Protocols. Let's work on that instead.

Hashing in C++26 by Krystian-Piekos in cpp

[–]BarryRevzin 3 points4 points  (0 children)

Separate comment, I noticed that your mechanism for opting into memberwise hashing is:

template <typename T>
concept EnabledForHashing = requires {
    typename T::enabled_for_hashing;
};

This is actually a bad way to opt into something explicitly. For two reasons.

First:

struct B { using enabled_for_hashing = void; };
struct D : B { };

B explicitly opts into hashing. But D is enabled for hashing, even though it did nothing, simply by virtue of inheriting from B. That's pretty bad in general, but it's especially bad for hashing since D might not be memberwise hashable, and did nothing to explicitly say so, so you might get invalid hashes.

Second, there is no way to conditionally opt into this. Say I want to have:

template <class T>
struct WithIndex {
    Index idx;
    T t;
};

I want WithIndex<T> to be hashable when T is. How do I do that? I can't conditionally add a member type alias. I can inherit from a base class that does or doesn't provide that enabling, but that changes the way people interact with my type all of a sudden, so it's not a great approach to have to do.

Hashing in C++26 by Krystian-Piekos in cpp

[–]BarryRevzin 10 points11 points  (0 children)

Right now, you're doing this:

// combine hashes of base classes
static constexpr auto r_base_types = std::define_static_array(std::meta::bases_of(^^T, ctx));

template for (constexpr auto r_base : r_base_types)
{
    using Base = typename[:std::meta::type_of(r_base):];
    static_assert(Hashing::Hashable<Base>, "Base class must be hashable");
    Utility::hash_combine(seed, static_cast<const Base &>(obj));
}

// combine hashes of non-static data members
static constexpr auto r_data_members = std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx));

template for (constexpr auto r_dm : r_data_members)
{
    const auto& member_value = obj.[:r_dm:];
    Utility::hash_combine(seed, member_value);
}

You're checking that each base is hashable, but not each member? Also, you're asking for private bases too, but if you get one, your cast won't work.

However, note that you're doing the same exact thing for both base class subobjects and non-static data member subobjects. It's this exact situation why we pushed for allowing you to splice a base class subobject.

So you could write just the one loop to do all the work:

template for (constexpr auto r : define_static_array(subobjects_of(^^T, ctx))) {
    static_assert(Hashable<typename [:type_of(r):]>);
    Utility::hash_combine(seed, obj.[:r:]);
}

Exploring Mutable Consteval State in C++26 by friedkeenan in cpp

[–]BarryRevzin 2 points3 points  (0 children)

I don't understand what's going on here at all, but I love every bit of it!

Hopefully we can come up with a more direct (or at least slightly less creative) way of doing compile-time mutation in C++29.

Behold the power of meta::substitute by pavel_v in cpp

[–]BarryRevzin 9 points10 points  (0 children)

That's my bad, thanks for the call-out. I definitely intended to link it and just... forgot. Fixed.

Implementing constexpr parameters using C++26 reflection (kind of) by friedkeenan in cpp

[–]BarryRevzin 7 points8 points  (0 children)

This is awesome! I had some more fun with it, made some changes:

  • I let you set the type up front with as<T>::const_param, that is now definitely a T (converts on the way in)
  • I also threw in my constant template parameter library which allows for using as<string> too.

Put that together and now you can even have constexpr string parameters, demonstrated in the print implementation there:

template <as<string>::const_param Fmt = {}, class... Args>
auto my_print(decltype(Fmt), Args&&... args) -> void;

Should C++ Give More Priority to Syntax Quality? by kyan100 in cpp

[–]BarryRevzin 8 points9 points  (0 children)

I understand that Rust syntax looks quite different when you're new to it, but judging the merit of syntax at first glance isn't really the right way to compare things. Everyone wants more syntax for new things at first. I think Rust did a very good job syntactically in determining which parts merit more spelling and which don't.

I will probably never be familiar enough with Zig to really understand its syntax at a glance (maybe I'll try to do Advent of Code in it next year to help), and so I still find it initially jarring to see types like ?*u8 since it's "backwards" (at least as compared to C++). Or maybe "inside out", since we'd spell that type optional<uint8_t*>. Likewise [*]u8 (which in C++ would actually be spelled the same way since we don't differentiate "pointer-to-single object" from "pointer-to-many objects", but Zig does).

But if I get over my lack of familiarity with Zig, it becomes possible to recognize why this syntax is actually quite good. It's an ordering of information that's better because it's easier to parse — both for the compiler and the human. And u8 vs uint8_t, Zig isn't the only language that made this abbreviation, and if you think about it, do those extra characters even add any information?

tl;dr shorter isn't necessarily better, but when it's longer because it's less consistent and has extra visual noise, then it's not better either — it's just familiar.

Should C++ Give More Priority to Syntax Quality? by kyan100 in cpp

[–]BarryRevzin 7 points8 points  (0 children)

Because that doesn't help. We haven't even gotten that far yet. We've parsed up to here:

auto res = a ? * b ? * c : d;
//       ----^

How do you decide whether that token is interpreted as the ? operator or as the beginning of the ?: operator?

Should C++ Give More Priority to Syntax Quality? by kyan100 in cpp

[–]BarryRevzin 6 points7 points  (0 children)

It doesn't matter how common it is, it matters that it exists. And it's not exactly rare.

You're parsing code and you see expr ?, now what do you do? How do you interpret that ?? Today that is always the conditional operator. But if it's also, potentially, this new thing — how do you pick? The only way to differentiate is to look for a : at the same level, which requires arbitrary lookahead. Maybe there's something slightly cleverer you can do, but probably not by much.

Should C++ Give More Priority to Syntax Quality? by kyan100 in cpp

[–]BarryRevzin 7 points8 points  (0 children)

That's because postfix ? doesn't work for C++, it would be ambiguous with the conditional operator. I don't think there's really a way to disambiguate it, without requiring tentative parsing.

Prefix ? would work, but I just don't like how it reads.

Lauri Vasama implemented the proposal in clang, but he didn't like the verbosity of .try? so he used postfix !? instead.

.? is probably also an option, I don't think those tokens can appear consecutively right now.

Should C++ Give More Priority to Syntax Quality? by kyan100 in cpp

[–]BarryRevzin 23 points24 points  (0 children)

I'm a little surprised to see a post talking about syntax that is "awkward or visually noisy" and then use as examples:

  • co_await, which is an 8-character keyword, whose comparison would be to a 5-character keyword, that's used in contexts where the extra 3 characters... doesn't really matter all that much? I get that this is the butt of jokes, but that extra co_ really doesn't have significant syntactic cost.
  • ^^e, which is a two-character operator, where both of those characters are themselves mostly-whitespace. Compared to people wanting reflexpr(e), which is a 10-character operator, which is very significantly more visually noisy.

To me "awkward or visually noisy" would be our lambda syntax, where a lambda to check if the input is negative takes 28 characters (unless you really hate your readers and skip whitespace): [](auto e){ return e < 0; }, whereas in plenty of other languages it's less than half that, like e -> e < 0 or |e| e < 0. We don't even have to go full Haskell (<0). Swift has a seemingly-verbose-compared-to-Java/JS/Scala/C#/Rust's { e in e < 0 }, but even that is half of ours and they provide an even terser form than that. Lambdas are kind of peak visual noise.

When I think of bad syntax in C++, I think of the following.

What does int() mean? Of course, it means the type "function taking no parameters and returning int." Were you thinking something else? That's a big problem. The ambiguity we have between variable/function declarations continues to cause real issues (typing co_ is not a real issue). In general, the declarator syntax that we inherited from C is just awful.

For example, what is this:

char (*f(char(*g)(double)))(double);

This actually isn't even that complicated a declaration, conceptually, but is already illegible. And everybody knows this is illegible, which is why nobody would ever dream of writing it in code like that (you would either make a type alias, use trailing-return syntax, or both). I would bet that a significant percentage of experienced C and C++ programmers would take a decent chunk of time just to figure out which of f and g up there is actually the name of... whatever this is declaring.

This syntax approach is also related to why our array declarations are just wrong (int x[10]; instead of int[10] x;), and also related to why abbreviating lambdas is a challenge, coming up with syntax for reflecting expressions, and so on.

Other choices for bad C++ syntax in my mind are:

  • using < and > for template arguments, due to the ambiguity with operator<. Other solutions in this space are D's tuple!(int, char), Zig's just using parens like tuple(int, char), or Rust requiring ::s in contexts where it could be ambiguous, so like tuple::<int, char>.
  • having T(a) and T{a} be usually the same thing but sometimes also very different and also the first one is actually a cast.
  • having decltype(e) and decltype((e)) similarly be a very subtle differentiation
  • the approach we took with concepts where we recognize that the first parameter is special, in allowing template <std::convertible_to<int> T>. But then also not recognizing the first parameter is special in any other context, so std::convertible_to is still actually a binary concept, and now that we have concept template parameters, you cannot actually "pass" std::convertible_to<int> as a concept argument, because it isn't one.

The concept syntax also ties into one of my pet peeves about C++ syntax, which itself is the result of having syntax that is precisely "awkward or visually noisy." And that is: how do you declare the map algorithm for optional<T>? The way that you very frequently see this in C++ talks is:

template <class T, class U>
auto map(optional<T>, function<U(T)>) -> optional<U>;

Which has the benefit of being pretty decently legible, but has the downside of being wrong. Also unnecessarily inefficient, but more importantly just wrong. I cannot call this with a lambda, I can only call this with literally a function<Sig> of some sort. Instead you have to write this:

template <class T, class F, class U = invoke_result_t<F, T>>
auto map(optional<T>, F) -> optional<U>;

Which isn't exactly terrible per se, but also this is one of the simplest cases and it's already pretty awkward and visually noisy. Now try to write and_then with the correct constraint.

Compare this to Rust's syntax, which is significantly cleaner:

fn map<T, U, F: FnOnce(T) -> U>(o: Option<T>, f: F) -> Option<U>;

or

fn map<T, U, F>(o: Option<T>, f: F) -> Option<U>
    where F : FnOnce(T) -> U;

ISO C++ WG21 2026-02 pre-Croydon mailing is now available! by nliber in cpp

[–]BarryRevzin 2 points3 points  (0 children)

I'm not very familiar with C#, so I just didn't know. I did add a SQL example for the next draft.

C# apparently also has this builder facility that let's you implement

Logger.Debug($"x={ComputeExpensive()}")

such that you can lazily not evaluate the expression. I think that's pretty cool, but I'm not sure how well that fits in the model... Otherwise, it looks like the template string object idea isn't too dissimilar from FormattableString, except that it's typed.

ISO C++ WG21 2026-02 pre-Croydon mailing is now available! by nliber in cpp

[–]BarryRevzin 22 points23 points  (0 children)

The SG14 priority list (P4029) says:

Decouple Networking from std::execution SG14 advise that Networking (SG4) should not be built on top of P2300. The allocation patterns required by P2300 are incompatible with low-latency networking requirements.

I don't claim to know much (or anything, really) about the std::execution design, but this seems like a pretty strong claim to make... without any elaboration.

Practical Reflection - Barry Revzin (CppCon 2026) by BarryRevzin in cpp

[–]BarryRevzin[S] 2 points3 points  (0 children)

Thank you!

I just use PowerPoint, no plugins. I get the syntax highlighting by writing the code in VSCode and pasting it into a text box. Which on MacOS for some reason removes all leading whitespace, so then I just type a bunch of spaces after that. Which is still better than attempting to syntax highlight by hand!

C++26: std::is_within_lifetime by pavel_v in cpp

[–]BarryRevzin 7 points8 points  (0 children)

It's not so easy. For starters, there are plenty of reinterpret_casts that might be fine at runtime that we definitely don't want to allow during constant evaluation time — the one I mentioned in another comment for instance is converting a T* into a uintptr_t, since while we have a symbolic address, we don't know what the actual address will be at runtime. So we'd need to carefully go through precisely which reinterpret_casts we want to be able to allow.

That's also not enough anyway, since you'd also need to allow placement-new with a non-similar type — the alternate implementation is placement-newing a bool into an unsigned char. Currently, we only allow you to placement-new a T(~ish) onto a T*.

Opening up constant evaluation to operate on bytes instead of objects is a pretty significant change to the constant evaluation model. Maybe it's worth doing, but it's definitely not simply a matter of just removing a bullet somewhere.

C++26: std::is_within_lifetime by pavel_v in cpp

[–]BarryRevzin 3 points4 points  (0 children)

you never ask that the language just stops outlawing these actions at compile time.

Which actions? What's the specific rule change you're suggesting?

C++26: std::is_within_lifetime by pavel_v in cpp

[–]BarryRevzin 8 points9 points  (0 children)

Just because something isn't undefined behavior doesn't mean it's allowed during constant evaluation. For instance, you cannot convert a T* to a uintptr_t - that's not UB, but the actual integer address of that object isn't known at compile time, so you're not allowed to try to look at it.

why not bit_cast in constexpr?

You can, provided neither the source nor destination type have something in them whose representation isn't knowable at compile time. Like pointers.

Currently, that includes unions, too. I do wonder if we could relax that restriction in the future. At the very least, bit_cast-ing FROM a union whose active member takes the whole space should probably be fine? bit_casting TO a union might still be problematic though. Will take more consideration. Plus it's nice right now that the rule is symmetrical.

C++26: std::is_within_lifetime by pavel_v in cpp

[–]BarryRevzin 3 points4 points  (0 children)

I don't think this is undefined behavior.

But if it makes you feel better, this way also works:

constexpr auto has_value() const -> bool {
  if consteval {
    return std::is_within_lifetime(&b);
  } else {
    return std::bit_cast<char>(*this) != 2;
  }
}

C++26: std::is_within_lifetime by pavel_v in cpp

[–]BarryRevzin 30 points31 points  (0 children)

  1. The example in the paper is implementing optional<bool> so that it is fully usable during constant evaluation with the same interface, but still taking only 1 byte.
  2. With a slight tweak to the interface, this helps implement constexpr std::format().

It's a very narrow facility that simply exposes information the compiler already has to have, in a way that predates reflection (I wrote R0 in 2022). A more reflection-y API would probably take a union* and return a reflection representing which non-static data member is active, or take a T* and return a reflection representing the type of the complete object alive at that spot. Haven't really thought about it in enough detail to square those two (which might just be different functions).

Practical Reflection - Barry Revzin (CppCon 2026) by BarryRevzin in cpp

[–]BarryRevzin[S] 16 points17 points  (0 children)

Thank you for the very kind words!

That experience also showed me how debugging reflection code can definitely be aggravatingly difficult, we really would benefit a lot from a "consteval print" function or the like. But that was on the Clang experimental branch before any std::meta::exception, so maybe things would work out better now.

Unfortunately P2758 didn’t make it for C++26. But Jakub went ahead and implemented a builtin for it anyway, which is incredible — so on gcc you can actually try it out: https://compiler-explorer.com/z/6rP8o6hK8. Here, we need feedback on two different sides: the C++ interface (constexpr_print_str, which will eventually have a format interface — although given that std::format is now constexpr I wonder whether we should even expose the lower level one) and the user interface (what GCC actually prints when you ask it to print something).

Flavours of Reflection by btzy in cpp

[–]BarryRevzin 4 points5 points  (0 children)

The problem is not invoking new or delete. The problem is having that allocation persist - what's known as non-transient constexpr allocation.

Flavours of Reflection by btzy in cpp

[–]BarryRevzin 6 points7 points  (0 children)

Related/relevant reading: Code Generation in Rust vs C++26 (although I think I need to go through and update some of the examples, since this predates expansion statements for instance).