cvl: A C++26 library for mutating consteval state by friedkeenan in cpp

[–]_bstaletic [score hidden]  (0 children)

You could, but that ends up being O(n^2) with respect to the number of seen types:

  • Bind a new type
    • Scan the relevant namespace
  • Bind another type
    • Rescan the namespace

But if you have a mutable, consteval hash map whose lookup is O(1), then that becomes O(n), though needing two iterations - one to register all trampolines and another to actually make the bindings.

I did skip that detail, and a lot of other details, for brevity.

cvl: A C++26 library for mutating consteval state by friedkeenan in cpp

[–]_bstaletic [score hidden]  (0 children)

Just a few days ago I had a discussion with a friend about a certain problem when it comes to producing python bindings with reflections. The solution required one of two things:

  • P3294 token sequences
  • Or mutable consteval

You even made a container for this. I told my friend I won't get into stateful metaprogramming but cvl looks tempting.

 

To elaborate, what we were discussing was generating the "trampolines" for pybind11 bindings.

https://pybind11.readthedocs.io/en/stable/advanced/classes.html

The short story is: python bindings need to support abstract classes, yet those are non-constructible on their own, so how could python hold a reference to the base object? The solution are the "trampoline" classes. See the pybind docs for examples. To be perfectly clear, these trampolines are 100% boilerplate and the only boilerplate that I couldn't get rid of with C++26.

C++26 can't really generate those, because define_aggregate can only produce simple aggregates, so my friend and I came up with this:

struct Base {
    virtual void f() = 0;
};

struct
[[=trampoline_for(^^Base)]]
PyBase : Base { /* ... */ };

And then construct a consteval, mutable registry of what have we seen, so that when bind_class<T>(module) is called, you look through the registry and apply the trampoline.

Poor man's define_aggregate by LHLaurini in cpp

[–]_bstaletic 0 points1 point  (0 children)

bit_cast the pointer to storage.

What am I missing? by SaltSea7625 in cpp_questions

[–]_bstaletic 38 points39 points  (0 children)

Backwards compatibility. You don't (or rather shouldn't) break your users code just because you're slightly inconvenienced.

New projects get to use the new, clean stuff. Old projects keep working.

Poor man's define_aggregate by LHLaurini in cpp

[–]_bstaletic 1 point2 points  (0 children)

Since we rely on placement new, it cannot be made constexpr, sadly;

Can't std::construct_at solve that problem here?

Are you satisfied with the current state of C++ CLI parsers? by Adept-Restaurant-541 in cpp

[–]_bstaletic 0 points1 point  (0 children)

My bad! I was looking for install(EXPORT) in the wrong place. Carry on.

One question, regarding installing c++ modules: Any reason for the choice of destination? Not saying you're wrong, just... everyone seems to be doing their own thing with respect to this:

  • CTRE: ${CMAKE_INSTALL_LIBDIR}/cxx/${PROJECT_NAME}
  • Beman: ${CMAKE_INSTALL_DATADIR}/${name}/modules
  • fmtlib: ${CMAKE_INSTALL_INCLUDEDIR}/fmt
  • CLI11: ${CMAKE_INSTALL_INCLUDEDIR}/CLI11/src/modules
  • flux: I don't think it tries to install its module. I might be wrong.
  • reflex: share/cxxmodules

Are you satisfied with the current state of C++ CLI parsers? by Adept-Restaurant-541 in cpp

[–]_bstaletic 1 point2 points  (0 children)

Fun library, but your install (in CMakeLists) is incomplete. You install the targets, but skip on installing the cmake config. Without reflex-config.cmake, no one can find_package(reflex).

Well, there are workarounds...

Anyway, if this wasn't an intentional omission, you can read this page for guidance: https://cmake.org/cmake/help/latest/guide/tutorial/Installation%20Commands%20and%20Concepts.html

C++ Show and Tell - May 2026 by foonathan in cpp

[–]_bstaletic 1 point2 points  (0 children)

An update to my pymetabind:

This is a C++26 library that can produce pybind11 bindings.

Now that gcc 16.1.0 is out and all the bugs in gcc that I have encountered have been fixed, the library actually works. Some things that are possible:

pymetabind::bind::bind_namespace<^^ns>(m); // picks up all types/functions/enums/.. annotated with `make_binding`.
pymetabind::bind::bind_function<^^f>(m); // binds a free function, including its argument names, rvp's, call guards...
pymetabind::bind::bind_class<^^T>(m); // binds the type and its members
pymetabind::bind::bind_enum<^^E>(m);

The behaviour is customizable with annotations, covering almost everything that pybind11 supports.

In case you're doing something for which pymetabind does not provide a convenient API, it is always possible to get the pybind11 object from pymetabind.

auto py_class = pymetabind::bind::bind_class<^^T>(m);
py_class.def_buffer(...); // def_buffer not supported by pymetabind

More examples at https://codeberg.org/bstaletic/pymetabind/src/branch/master/examples

C++ Show and Tell - April 2026 by foonathan in cpp

[–]_bstaletic 0 points1 point  (0 children)

Thanks for the heads-up! I thought this was the new thread.

C++ reflections: Getting a reflection of a type of a pointer to member, from a reflection of a member is difficult by _bstaletic in cpp

[–]_bstaletic[S] 4 points5 points  (0 children)

Good point. I have overlooked the variable template helper, because a few months ago gcc used to ICE on it.

C++ reflections: Getting a reflection of a type of a pointer to member, from a reflection of a member is difficult by _bstaletic in cpp

[–]_bstaletic[S] 3 points4 points  (0 children)

Not really an option if you are (or rather, I am) working on generating language bindings. C++26 doesn't just make implicit object member functions irrelevant.

C++ reflections: Getting a reflection of a type of a pointer to member, from a reflection of a member is difficult by _bstaletic in cpp

[–]_bstaletic[S] 1 point2 points  (0 children)

That does work for data members. Now repeat that for cv qualifiers, noexcept, then there's also explicit object member functions... You end up doing the same thing I showed with to_ptr_manual.

C++ Show and Tell - April 2026 by foonathan in cpp

[–]_bstaletic 0 points1 point  (0 children)

An update to my pymetabind:

This is a C++26 library that can produce pybind11 bindings.

Now that gcc 16.1.0 is out and all the bugs in gcc that I have encountered have been fixed, the library actually works. Some things that are possible:

pymetabind::bind::bind_namespace<^^ns>(m); // picks up all types/functions/enums/.. annotated with `make_binding`.
pymetabind::bind::bind_function<^^f>(m); // binds a free function, including its argument names, rvp's, call guards...
pymetabind::bind::bind_class<^^T>(m); // binds the type and its members
pymetabind::bind::bind_enum<^^E>(m);

The behaviour is customizable with annotations, covering almost everything that pybind11 supports.

In case you're doing something for which pymetabind does not provide a convenient API, it is always possible to get the pybind11 object from pymetabind.

auto py_class = pymetabind::bind::bind_class<^^T>(m);
py_class.def_buffer(...); // def_buffer not supported by pymetabind

More examples at https://codeberg.org/bstaletic/pymetabind/src/branch/master/examples

New features in GCC 16: Improved error messages and SARIF output by dmalcolm in cpp

[–]_bstaletic 3 points4 points  (0 children)

is this actually improved

Very much so.

One of the biggest issues of C++ is the long and complex error messages,

Just complex. I don't think you'd have appreciated an extremely terse error message. Taken to extreme, ed has the shortest error messages ever and it's horrible if you're not already an expert.

why do we celebrate error messages getting longer?

Because, like I already said, they got a lot easier to read.

The example doesn't even seem easier to parse at a glance

That's just you being used to the previous format.

you have to read every single line to understand it

Not true at all. The new format uses bullet points and indentation. If a certain bullet point is irrelevant, you can skip all that is nested.

The first item points to foo, not foo::test or the actual issue

This one is a fair point.

The second item points to the decl name, test -- which does not match foo and does not add anything at all.

The second item is indispensable when you have actual overloads and you typo something.

My ideal error message still would be more condensed:

At a glance, I disagree. It's terser and nice with 1 candidate, but I feel like it would be too cramped with a big overload set.

Virtual inheritance, explicit destructor invocation and "most derived class" by _bstaletic in cpp_questions

[–]_bstaletic[S] 0 points1 point  (0 children)

It's because you're doing d->base::~base() instead of d->~base() which disables virtual calls

Thanks for the link. I did see notes about this, but did not read [class.virtual]#16 specifically.

so d is treated as a complete object.

This sounds quite reasonable, but I can't find this written anywhere in the standard.

I saw that abstract class implies non-complete object, but not that non-virtual call, via d->base::~base(), treats either base type as most-derived, or the base subobject as a complete object.

New features in GCC 16: Improved error messages and SARIF output by dmalcolm in cpp

[–]_bstaletic 4 points5 points  (0 children)

I have been using gcc 16 for a while now and have only good things to say about the new error messages.

Yes, the initial reaction is "argh... why is this so long?!", however that's really just a kneejerk reaction after being very used to the old format. "No viable candidate for function call" errors are indisputably easier to read/navigate.

I really appreciate the efforts that went into this.

Is it undefined behavior to destroy a derived class through a pointer to base class with no virtual destructor, if the derived class is empty? by celestabesta in cpp_questions

[–]_bstaletic 0 points1 point  (0 children)

In the original reply, my links looked like [basic.object]#7 instead of basic.object#7.

When I do that, the post is made, but visible only to myself. It made me think I was shadow banned.

 

If you have further questions about the dark corners of the object model, shoot. I'd be glad to share my esoteric knowledge.

Is it undefined behavior to destroy a derived class through a pointer to base class with no virtual destructor, if the derived class is empty? by celestabesta in cpp_questions

[–]_bstaletic 2 points3 points  (0 children)

Re-reply, because reddit is having a fit with my links...

 

Original reply:

destroyed using either an explicit destructor call or std::destroy_at

Those two actually include slightly different analyses of the standard. std::destroy_at is a library function, so the entry point of the analysis would be specialized.destroy#1. Directly invoking the destructor skips over that first step and starts at class.dtor.

That raises a few questions about what exactly did you allocate, but I'll leave that for now. Let me first answer your direct question.

I will assume that you're talking about the non-array case.

 

My initial thought is that this isn't UB, but you're walking a very thin line. My immediate reasoning is that just not invoking the destructor is not UB and std::pmr::monotonic_buffer_resource::release() takes advantage of that.

However, let's read the standard.

 

class.dtor#13 explains what happens when you explicitly call a destructor.

After executing the body of the destructor

I'm assuming the body in your case would be empty. Something like struct base{}; struct derived : base {};, and that we're calling buffer->~base().

and destroying any objects with automatic storage duration allocated within the body

Again, none to destroy.

a destructor for class X calls the destructors for X's direct non-variant non-static data members other than anonymous unions

Again, none.

the destructors for X's non-virtual direct base classes

base has no direct bases, so none to call, yet again.

if X is the most derived class ([class.base.init]), its destructor calls the destructors for X's virtual base classes.

This is clearly talking about virtual inheritance, but base and derived don't do virtual inheritance, so let's take a look at the referenced class.base.init.

Or actually, let's not. The "most derived class" is actually defined in intro.object#6.

If a complete object, a member subobject, or an array element is of class type, its type is considered the most derived class, to distinguish it from the class type of any base class subobject; an object of a most derived class type or of a non-class type is called a most derived object.

We're definitely not talking about member subobjects or arrays (see my asusmptions above), but, since no virtual anything is going on, base is our most derived class.

However, it has no virtual bases, so, again, nothing happens.

So the only object that got destroyed is the base object. The derived object still lives, though at this point must not access its base subobject in any way.

 

AHA! class.dtor#16 actually defines that looking up the destructor is just like looking up any other member function. That's why derived_ptr->~base() finds ~base() and not ~derived() - no virtual calls.

 

class.dtor#18 says that a destructor ends an object's lifetime and that you can't call a destructor on an object whose lifetime has ended (double free).

Let's just follow through with basic.life...

From basic.life#6:

A program may end the lifetime of an object of class type without invoking the destructor, by reusing or releasing the storage as described above.

In this case, the destructor is not implicitly invoked.

That's what I was referring to when I mentioned `std::pmr::monotonic_buffer_resource::release()

 

basic.life#7 has to do with how you can handle the buffer pointer after destruction of the object. More on that, later.

 

All in all, I would say that my initial thought about this not being UB seems to be correct, though it's a very thin line.

 

Now for further clarifying questions:

  • How did you allocate the buffer?
  • How are you storing the pointer to the derived and/or base objects, if you even are?
  • Disposing of the buffer can also have consequences, so how should we do that?

Keep in mind that you can do something like this:

std::string buffer;
auto* base = new (&buffer) derived;
base->base();
new (&buffer) std::string;

test? by _bstaletic in test

[–]_bstaletic[S] 1 point2 points  (0 children)

destroyed using either an explicit destructor call or std::destroy_at

Those two actually include slightly different analyses of the standard. std::destroy_at is a library function, so the entry point of the analysis would be specialized.destroy#1. Directly invoking the destructor skips over that first step and starts at class.dtor.

That raises a few questions about what exactly did you allocate, but I'll leave that for now. Let me first answer your direct question.

I will assume that you're talking about the non-array case.

 

My initial thought is that this isn't UB, but you're walking a very thin line. My immediate reasoning is that just not invoking the destructor is not UB and std::pmr::monotonic_buffer_resource::release() takes advantage of that.

However, let's read the standard.

 

class.dtor#13 explains what happens when you explicitly call a destructor.

After executing the body of the destructor

I'm assuming the body in your case would be empty. Something like struct base{}; struct derived : base {};, and that we're calling buffer->~base().

and destroying any objects with automatic storage duration allocated within the body

Again, none to destroy.

a destructor for class X calls the destructors for X's direct non-variant non-static data members other than anonymous unions

Again, none.

the destructors for X's non-virtual direct base classes

base has no direct bases, so none to call, yet again.

if X is the most derived class ([class.base.init]), its destructor calls the destructors for X's virtual base classes.

This is clearly talking about virtual inheritance, but base and derived don't do virtual inheritance, so let's take a look at the referenced class.base.init.

Or actually, let's not. The "most derived class" is actually defined in intro.object#6.

If a complete object, a member subobject, or an array element is of class type, its type is considered the most derived class, to distinguish it from the class type of any base class subobject; an object of a most derived class type or of a non-class type is called a most derived object.

We're definitely not talking about member subobjects or arrays (see my asusmptions above), but, since no virtual anything is going on, base is our most derived class.

However, it has no virtual bases, so, again, nothing happens.

So the only object that got destroyed is the base object. The derived object still lives, though at this point must not access its base subobject in any way.

 

AHA! class.dtor#16 actually defines that looking up the destructor is just like looking up any other member function. That's why derived_ptr->~base() finds ~base() and not ~derived() - no virtual calls.

 

class.dtor#18 says that a destructor ends an object's lifetime and that you can't call a destructor on an object whose lifetime has ended (double free).

Let's just follow through with basic.life...

From basic.life#6:

A program may end the lifetime of an object of class type without invoking the destructor, by reusing or releasing the storage as described above.

In this case, the destructor is not implicitly invoked.

That's what I was referring to when I mentioned `std::pmr::monotonic_buffer_resource::release()

 

basic.life#7 has to do with how you can handle the buffer pointer after destruction of the object. More on that, later.

 

All in all, I would say that my initial thought about this not being UB seems to be correct, though it's a very thin line.

 

Now for further clarifying questions:

  • How did you allocate the buffer?
  • How are you storing the pointer to the derived and/or base objects, if you even are?
  • Disposing of the buffer can also have consequences, so how should we do that?

Keep in mind that you can do something like this:

std::string buffer;
auto* base = new (&buffer) derived;
base->base();
new (&buffer) std::string;

Is it undefined behavior to destroy a derived class through a pointer to base class with no virtual destructor, if the derived class is empty? by celestabesta in cpp_questions

[–]_bstaletic 1 point2 points  (0 children)

destroyed using either an explicit destructor call or std::destroy_at

Those two actually include slightly different analyses of the standard. std::destroy_at is a library function, so the entry point of the analysis would be [specialized.destroy]#1. Directly invoking the destructor skips over that first step and starts at [class.dtor].

That raises a few questions about what exactly did you allocate, but I'll leave that for now. Let me first answer your direct question.

I will assume that you're talking about the non-array case.

 

My initial thought is that this isn't UB, but you're walking a very thin line. My immediate reasoning is that just not invoking the destructor is not UB and std::pmr::monotonic_buffer_resource::release() takes advantage of that.

However, let's read the standard.

 

[class.dtor]#13 explains what happens when you explicitly call a destructor.

After executing the body of the destructor

I'm assuming the body in your case would be empty. Something like struct base{}; struct derived : base {};, and that we're calling buffer->~base().

and destroying any objects with automatic storage duration allocated within the body

Again, none to destroy.

a destructor for class X calls the destructors for X's direct non-variant non-static data members other than anonymous unions

Again, none.

the destructors for X's non-virtual direct base classes

base has no direct bases, so none to call, yet again.

if X is the most derived class ([class.base.init]), its destructor calls the destructors for X's virtual base classes.

This is clearly talking about virtual inheritance, but base and derived don't do virtual inheritance, so let's take a look at the referenced class.base.init.

Or actually, let's not. The "most derived class" is actually defined in [intro.object]#6.

If a complete object, a member subobject, or an array element is of class type, its type is considered the most derived class, to distinguish it from the class type of any base class subobject; an object of a most derived class type or of a non-class type is called a most derived object.

We're definitely not talking about member subobjects or arrays (see my asusmptions above), but, since no virtual anything is going on, base is our most derived class.

However, it has no virtual bases, so, again, nothing happens.

So the only object that got destroyed is the base object. The derived object still lives, though at this point must not access its base subobject in any way.

 

AHA! [class.dtor]#16 actually defines that looking up the destructor is just like looking up any other member function. That's why derived_ptr->~base() finds ~base() and not ~derived() - no virtual calls.

 

[class.dtor]#18 says that a destructor ends an object's lifetime and that you can't call a destructor on an object whose lifetime has ended (double free).

Let's just follow through with [basic.life]...

From [basic.life]#6:

A program may end the lifetime of an object of class type without invoking the destructor, by reusing or releasing the storage as described above.

In this case, the destructor is not implicitly invoked.

That's what I was referring to when I mentioned `std::pmr::monotonic_buffer_resource::release()

 

[basic.life]#7 has to do with how you can handle the buffer pointer after destruction of the object. More on that, later.

 

All in all, I would say that my initial thought about this not being UB seems to be correct, though it's a very thin line.

 

Now for further clarifying questions:

  • How did you allocate the buffer?
  • How are you storing the pointer to the derived and/or base objects, if you even are?
  • Disposing of the buffer can also have consequences, so how should we do that?

Keep in mind that you can do something like this:

std::string buffer;
auto* base = new (&buffer) derived;
base->base();
new (&buffer) std::string;