omniverse theirix's Thoughts About Research and Development

Meeting C++ 2025 Trip Report

This November, I had the opportunity to attend the famous Meeting C++ conference. Located in Germany, it attracts many C++ developers not only from continental Europe, but also from the UK and the USA. I am grateful to the organisers, especially Jens Weller, for accepting my proposal to be part of such a great lineup of talks across five tracks during three days. So yes, it was huge.

Let me share my thoughts about a few talks and what I’ve found especially interesting.

Software and Safety keynote

Anthony Williams worked on the C++ concurrency standard, wrote a lot of articles and a book “C++ Concurrency in Action”, which is an excellent crash course into (non-)crashing multi-threaded programs. Now he explores the automotive industry, which has a special interest in concurrency and safety from undefined behaviour in C++.

I like the “Swiss Cheese” concept Anthony carried throughout his keynote talk, “Software and Safety”. It likens the system to a stack of Swiss cheese layers with randomly placed and sized holes in each slice, mitigating the overall risk. There is no silver bullet. You can only decrease the failure risk by having multiple techniques in place, like:

  • Design your system to minimise the potential for problems
  • Use static analysis
  • Test with potentially problematic input
  • Fuzz test
  • Use sanitisers and hardened libraries
  • Use contracts

It was a perfect start to the conference, providing the tone of safety for critical systems in C++. I’ve seen and spoken with many attendees working in highly regulated environments, such as automotive, aerospace, and healthcare. All of us would prefer these safety-critical systems to be written with these ideas in mind. For regulated financial institutions, it’s not that critical, but it’s also a key factor in ensuring a safe and secure language and foundational libraries.

From Acrobatics to Ergonomics - A Field Report on How to Make Libraries Helpful

Then I moved to an excellent and entertaining talk by Joel Falcou from INRIA on designing software libraries for scientists to use. It’s a different approach compared to writing libraries for hardcore C++ developers. You cannot just throw kilobytes of template errors at users without a reasonable explanation of the errors and, more importantly, actions to fix them from a user perspective. Joel explained how concepts, type systems, and proper API design can greatly simplify the developer experience. Joel believes that library developers should care more about users, and modern C++ makes it easier.

In Rust, developers have excellent diagnostics support from the compiler, and rich support for custom diagnostics is also provided to libraries via the form of annotate-snippets-rs crate and at the attribute-level with #[diagnostic::on_unimplemented] (docs). C++ compilers are becoming increasingly capable of reducing error clutter, but there is still a long way to go.

Also, a simple and intuitive design doesn’t have to be non-extensible. The speaker explained that intuitive means the incorrect usage is flagged clearly, and all components adhere to the Single Responsibility SOLID principles. Extensibility means that users should be able to combine pieces of the API and obtain sensible results. Power users should be able to customise their API usage to their own corner cases, and finally, developers should have a clear path to extend the API.

Lessons learned from the talk.

  • Use Concepts and static asserts to enforce API design
  • Use if constexpr and static asserts to simplify metaprogramming
  • Prevent problems earlier with proper diagnostics

Harnessing constexpr: A Path to Safer C++

Mikhail Svetkin spoke about memory safety. Undefined behaviour can be easily stopped by LLVM sanitisers, but it’s not the only way. Developers could start sprinkling constexpr into functions, allowing them to operate not only at runtime but also at compile-time. The main side effect of constexpr is the lack of undefined behaviour. So if your constexpr unit tests work on code, it’s free of undefined behaviour. It allows unit tests for such functions to run even before they are executed! Maxing this out requires some tricky techniques that Mikhail has shown, and the result is fascinating — unit test errors are displayed in your IDE before compilation.

The constexpr support has grown extensively during the last standards. I cannot resist mentioning this evolution list:

  • C++11: expression

  • C++14: variables, control flow, array

  • C++17: lambdas, string view, if constexpr

  • C++20: string, vector, virtual functions, try, new/delete

  • C++23: basic math, unique_ptr, optional, variant

  • C++26: sorting, floating-point math, atomic, hashes, exceptions

  • C++29: format

Monadic Operations in C++23

Robert Schimkowitsch delivered a great, well-structured and perfectly paced talk that brought functional concepts to engineers who used to develop in an imperative style.

The outcomes of this talk are to understand what functors and monads actually do, how to use monadic operations from the standard library and how to build a functional-style program based on this foundation.

It is a useful talk for whoever is not so familiar with monadic concepts. Users of Haskell and more practically oriented languages with functional concepts, like Rust or Swift, could also benefit from this knowledge to find the subtle differences in behaviour.

Asking Stoopid questions

The keynote, as it should be. Frances Buontempo, a seasoned math teacher and educator, explained how to teach people, how to learn, and how to tackle the learning process on one’s own. A philosophical keynote, with food for thought and references to my favourite cyberpunk writers, Neal Stephenson and William Gibson.

int != safe && int != ℤ

An insightful talk from Peter Sommerlad about another safety facet of C++ — undefined behaviour with integer overflow. Regulated environments like MISRA require careful handling of overflows and the resulting undefined behaviour.

Built-in types are limited in size (they are not math integers), we can do different things for, say, addition:

  1. Wrap them (-1 after 0xFFFFFFFFF)

  2. Saturate (0xFFFFFFFFF + 1 stays 0xFFFFFFFFU)

  3. Panic or throw

It’s an inherent property of machine types. Safety-aware languages like Rust have a variety of operations on the same integer type:

let mut x: u32 = 100;
assert!(x+1 == 101);
assert!(x.checked_add(1) == Some(101));
assert!(x.strict_add(1) == 101); // panics on overflow
assert!(x.overflowing_add(1) == (101, false));
assert!(x.saturating_add(1) == 101);

x = 0xFFFFFFFF;
// assert!(x+1 == 0); // panics in Debug mode, overflows in Release
assert!(x.checked_add(1) == None);
// assert!(x.strict_add(1) == 0); // panics on overflow
assert!(x.overflowing_add(1) == (0, true));
assert!(x.saturating_add(1) == 0xFFFFFFFF);    

To be fair, it’s not MISRA-compatible, since it provides a set of specific requirements, such as not mixing signed and unsigned types, restricting type promotions and limiting operations for signed/unsigned types

The idea Peter proposed is to use special wrapper types that simulate the ℤ algebraic group (signed integers) with arbitrary precision and the mentioned operation semantics without overflows. His set of libraries are header-only C++20, providing the following types:

#include <pssodin.h>
constexpr pssodin::cui32 x{10}, y{20}, z{x+y};    
static_assert(z == pssodin::from_int(30));

It also works with primitive types:

int32_t x{10}, y{20}, z{0};
assert(!pssodin::add_overflow(x, y, &z) && z == 30);

A distinctive feature of this library is compile-time checks for type eligibility for the majority of operations. Hence, the library is almost entirely constexpr, greatly simplifying testing for undefined behaviour. The API design is based on lean enum classes, which are more performant than a class with an integer data member.

What about having some of these features in a C++ standard? Not so good as with external libraries and other languages. With C++26, some saturation helpers are available in the std library, but not overflow:

static_assert(std::add_sat(10, 20) == 30);

C++20 also improved the situation a bit with tiny helpers std::cmp_equal to avoid common pitfalls with signed/unsigned comparison:

static_assert(-1 > 1U);
static_assert(std::cmp_less(-1, 1U));
static_assert(-1 == 0xFFFFFFFFU);
static_assert(std::cmp_not_equal(-1, 0xFFFFFFFFU));

So, for developers dealing with integer arithmetic, MISRA requirements, and older standards, Peter’s library is a godsend. Impressive work.

The Missing Step: Making Data Oriented Design One Million Times Faster

A keynote-style talk from Andrew Drakeford. He explained how a methodological and systematic approach allows for solving complex problems in financial applications. He outlined ideas from George Polya’s book “How to Solve it”] (goes to my reading list).

It’s much better to listen to this talk and experience a problem-solving journey with the speaker.

Still, I’d like to mention key takeaways (credits to the author):

  • Design addresses a highly dimensional problem.
    • Logical algorithm design
    • Physical optimising spatio-temporal memory use patterns
    • Idiosyncratic aspects of the actual problem itself.
  • Working through the problem space in a structured way
  • Always look to expand the context of your thinking around problem areas
  • Draw pictures and solve easier ancillary problem

Unlocking the Value of C++20 Features

A day wrapped with a good overview by Alex Dathskovsky from ScyllaDB of the new C++20 and C++23 features, spanning two talks. It’s handy to have an overview and a reminder to evaluate some new C++ features, given the vast scope of the recent standard. There are a few things I’ve found especially useful and interesting.

Generic lambdas and simplified metaprogramming:

auto gen_lambda = []<typename T>(T a, T b, auto c) {
  return a + b + c;
};

Constexpr’s are everywhere, even in virtual hierarchy — what a blasphemy:

struct Animal {
  constexpr ~Animal() noexcept {};
  constexpr virtual std::string say() const = 0;
};

struct Cat : Animal {
    constexpr virtual std::string say() const override { return "Meow"; }
};

int main() {
  static constexpr const Cat cat;
  constexpr const Animal& animal = cat;
  std::cout << animal.say();
}

Transparent comparison for containers. It’s a tricky technique that unlocks more performance while working with standard containers:

std::set<std::string, std::less<>> foo;

Since C++20, it is allowed to specify a transparent hasher for hashing containers, skipping key copy:

struct transparent_hash {
{
  using is_transparent = void; // a key ingredient

  size_t operator()(const char* s) {
    return std::hash<std::string_view>(s);
  }
  size_t operator()(std::string_view s) {
    return std::hash<std::string_view>(s);
  }
};
std::unordered_set<std::string, transparent_hash, std::equal_to<>> foo;

// no intermediate std::string constructed
foo.find("a C string");
foo.find(std::string_view{"a string view"});

Speed for free - current state of auto-vectorizing compilers

Who doesn’t want speed for free? Stefan Fuhrmann agrees and explains how the automatic vectorisation works in modern compilers. Of course, developers can call compiler intrinsics or provide compiler hints to insert SIMD instructions here and there. It turns out tha modern clang with -O2 and gcc with -O3 optimisation levels can vectorise simple scalar loops. They are still unable to produce masked instructions to care about data which doesn’t fill the whole SIMD lane. So your typical loop over data should be split into two: a main loop for evenly sized chunks and a tail loop. Compiler engineers do a fantastic job of optimisation. Figuring out whether you can rely on them and bring it to the audience — it’s a great job too!

Why managing C++ dependencies is hard (and what to do about it)

Kerstin Keller presented the journey of one software company from a bunch of prebuilt libraries committed to VCS to a modern Conan 2.0-based build system, with a special emphasis on Windows. It’s always great to hear about experience with Conan across different areas and for different platforms.

Sanitize for your Sanity: Sanitizers tools for Modern C++

Last but not least — my humble talk.

Presenting at Meeting C++

The slides are here on my website. The recordings would eventually be available, too.

According to the Swiss Cheese concept, it is mandatory to use multiple levels of defence. Type system, linters, undefined behaviour check via constexpr sprinkles — it is only a part of the whole deal. At runtime, you can do much more with sanitizers (address sanitizers, thread, memory and undefined). I explained the best ways to work with them, especially how to tailor them to a complex mix of libraries, instrumented and non-instrumented, which is an extremely common but usually overlooked scenario. Also, I tackled a problem of multi-language integration of Rust and C++ code, when using sanitizers to track cross-language problems. So it’s not a choice between Rust and C++, it is a question of making them work better together. As usual, when I am anxious, I speak very fast, which is definitely a problem when delivering complex topics to the public, so there is room for improvement. Nevertheless, slides are always available online, and I am happy to help and give advice on memory safety and sanitizers usage.

Conclusion

This year was turbulent for the world, the industry, developers, and conference organisers worldwide. Why even attend conferences and read books when LLMs could provide all the knowledge of mankind (and mistakes, too)? Despite this, Meeting C++ was filled with great talks, enthusiastic speakers, and curious attendees. The most appealing to me was a shared sense of C++ moving forward — especially towards greater safety. I also shared my knowledge in the talk, and I hope it will help make C++ a better and safer place.

It was terrific to meet developers from many industries, with expertise across different languages and domains. It is energising and makes you feel more confident about the community, the ecosystem, and the people behind it.

See you all next time!


uv for fast wheels

To build or not to build?

You have a Python package. It solves a technical or business problem. You have decided to share it with the open-source community. Okay, just set up GitHub Actions and publish it to the PyPI. Periodically, you carve a new version and publish it. Everybody is happy – developers, consumers, and the community.

Then you suddenly discover that Python is slow. It is perfectly fine for many use cases, but yours is doing CPU-intensive work. You think it’s worth shifting CPU-intensive work from the interpreted language to a native extension written in C, C++ or Rust. There are a few different approaches to do this – Cython, CFFI, pure low-level CPython extensions. CFFI is the most widely used. You need to write native code in C, build it somehow into a library, load the library from Python and call it while adhering to call conventions and ownership semantics.

The problem is how to build and package a binary distribution. Different projects go in different directions. Here lies my experience of trying and using them.

Poetry

Poetry prefers having explicit build scripts, and the Poetry build backend is aware of them.

[build-system]
requires = ["poetry-core", "setuptools"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.build]
script = "scripts/build.py"

The build script is a simple Python module invoking setuptools.command.build_ext and cffi or cython. The downsides of this approach – it is still unofficial and unstable, and the behaviour changed with Poetry 2.

Setuptools

With old, good setuptools, it is easy to use setuptools’ official and supported integrations with ext_modules using cythonize or ffibuilder using the following setup.py:

setup(
    setup_requires=["cffi>=1.0.0"],
    cffi_modules=["scripts/build.py:ffibuilder"],
    install_requires=["cffi>=1.0.0"],
)

Worked like a charm for decades, but now it’s outdated. Even with dependency control via pip freeze you will run into many well-known Python packaging problems.

uv

What if you want to combine a modern packaging tool like uv and build binary distributions?

uv uses pyproject.toml for project configuration. The main benefit of the pyproject format is the standardisation of storing metadata for a project, build backend and external tools. It’s a big step forward from the imperative setup.py. It allows not only storing metadata, but also simplifying tools and significantly improving performance, because dependency resolution no longer requires evaluating Python code in setup.py.

Of course, all the dependencies – build or runtime – can be specified in the same file. uv has a great build backend uv_build (used by default) – extremely fast (because, you know, it’s in Rust :zap:) and reliable. It fully supports the pyproject.toml standard, including the standard project tag.

Binary distributions and uv

Unfortunately, this uv_buildbackend only supports pure Python. For binary extensions, the easiest way to overcome this limitation is to use a setuptools build backend instead.

[build-system]
requires = ["setuptools>=61", "cffi"]
build-backend = "setuptools.build_meta"

setuptools itself perfectly supports multiple formats for configuration – TOML.

[project]
dependencies = [ "numpy", "h3" ]

or setup.py:

setup(install_requires=[ "numpy", "h3" ])

But that’s not for all tools. All the integrated tools should decide for themselves where to get data from. For example, CFFI doesn’t support declarative pyproject.toml as a data source for cffi_module, which is sad because you have to store a setup.py file, albeit simple:

from setuptools import setup
setup(cffi_modules=["scripts/build.py:ffibuilder"])

Okay, now we’re back to setup.py. Poetry 1.x was able to generate setup.py with build instructions under the hood, so this approach is more or less familiar.

The most challenging part is to configure package discovery and file packaging properly. Here’s how we did it in the open-source timezonefinder package with @jannikm. It has a flat layout. Having a flat layout can bring unnecessary files into the package compared to the src layout, and that can be improved with the where = ["src"] directive. If you have a flat-layout, but still follow standard naming conventions, then automatic package discovery could work without any additional tweaks. It excludes typical directories like tests, examples, etc. To include new files, like built binaries in a module timezonefinder/inside_poly_extension, you have to include them in the packages directive explicitly:

[tool.setuptools]
packages = ["tests", "timezonefinder", "timezonefinder.inside_poly_extension"]

[tool.setuptools.package-data]
timezonefinder = ["**/*.json", "**/*.c", "**/*.h", ]

Since automatic package discovery is used, the build.py script cannot be stored at the root of the repository (it is in the exclusion list). Placing the script in the timezonefinder package eliminates this problem. Also, in this example, the tests are packaged too, because we need them in the distribution. The build script itself is simple and compiles a dynamic library from sources.

Essentially, the whole process of building the package is either

uv build --sdist

or

uv build --wheel

uv and CI

uv also simplifies CI setup for many projects.

If a project is pure Python, nothing special is required. Run tests, run a linter, make an sdist and upload it to PyPI. uv can streamline commands and simplify tool installation.

Binary packaging complicates the CI build. Now the project has multiple binary targets. Possible dimensions are cp39,cp310, cp311 for Python versions; musllinux or manylinux for ABI targets. There are two approaches to this.

First, the cibuildwheel tool. It manages the whole process automatically with a single GitHub Actions step by creating Docker containers for all permutations of specified Python versions and architectures. Inside each container, it runs a specified command (setuptools by default) and exports the produced wheels.

- name: "Build wheels"
    uses: pypa/[email protected]
    with:
      output-dir: dist
    env:
      CIBW_MANYLINUX_X86_64_IMAGE: quay.io/pypa/manylinux2014_x86_64:2025.03.09-1
      CIBW_MUSLLINUX_X86_64_IMAGE: quay.io/pypa/musllinux_1_1_x86_64:2024.10.26-1
      CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*"
      CIBW_BUILD_FRONTEND: pip
      CIBW_BEFORE_ALL_LINUX_MANYLINUX2014: yum install -y libffi-devel clang make
      CIBW_BEFORE_ALL_LINUX_MUSLLINUX_1_1: apk add --no-cache libffi-dev clang make

The second approach is to utilise a matrix build. uv can manage Python versions by itself by downloading prebuilt CPython binaries and executing scripts in their context. So the GitHub Actions build becomes even simpler:

strategy:
  matrix:
    python-version:
      - "cp310"
      - "cp311"
      - "cp312"
steps:
  - uses: actions/checkout@v4

  - name: Make wheel
    run: uv build --python $ --wheel

Of course, if you want to achieve manylinux or abi3 compatibility, you’ll need to use cibuildwheel in combination with uv.

You could also use the GitHub action astral-sh/setup-uv@v6, but the example cited above is the shortest version to demonstrate the flow.

Conclusion

uv provides an excellent experience when working with modern Python packaging. Lightning-fast dependency resolution and tool ergonomics are the main benefits.

Binary distribution is not a first-class citizen in uv and requires a fallback to setuptools build backend, while still using uv as a frontend and keeping its ergonomics.

The problem of standardised binary distribution building is not yet solved by any tool. Approaching this could be the next big step for the Python ecosystem, especially when Python is increasingly accelerated with native code.