What you can learn from being too cute

Why you should write code that you should never write

(Part 2 of N)

Daisy Hollman, Google

Pure Virtual C++ 2022
Twitter: @The_Whole_Daisy
Email: cpp@dsh.fyi

But why?

Goals for this talk

  • Have some fun geeking out about how weird C++ is sometimes
    • Sometimes you have to laugh to keep from crying
  • Learn something new about how C++ works
    • ...by using it in a way that's so bizarre that you can't forget it when it comes up in real code
    • (and learn the "right" way to do the same thing)
  • Learn about how to learn about the ways that dark corners of the language interact

Disclaimers

  • Don't use these tricks (directly) in "real" code
    • But do use them to learn things that will help you understand existing code
  • This is not a software engineering talk
    • Well-written code should be unsurprising
    • This talk is intentionally about code snippets that are surprising
  • I have a problem with my talks getting "too into the weeds"
    • 🤷🏼‍♀️ Sorry, this talk is all weeds 🌱
  • This talk is actually several mini-talks


Anyway...

Here we go!

🤷🏼‍♀️ 🌱 🌼

Trick 1: Assertions in constexpr contexts

Assertions in constexpr contexts

Cute C++ trick of the day: you can't use static_assert in a constexpr function for an expression that's dependent on function parameters ("constexpr" means "maybe this might be evaluated in a constant expression"). But you can just use assert as usual

What does constexpr mean?

What does constexpr mean?

It depends on which constexpr you mean!

  • constexpr variables
  • constexpr functions
  • constexpr if statements (a.k.a., if constexpr)
  • ...and more (we'll come back to this)

Different constexpr uses require different things to be constant expressions

  • constexpr variables require constant initializers
  • constexpr if statements require constant conditional expressions
  • constexpr functions require ???
    (Hint: it's not as simple as just "the function body")

What does constexpr mean (for a function)?

Look at how we can use it...

  • to initialize a constexpr variable...
  • in a static_assert...
  • as a non-type template argument...
  • ...as long as its arguments are also constant expressions!
  • But...constexpr functions can be used with runtime parameters!

What does "maybe" constexpr mean? (And why?)

What did I mean by "constexpr means maybe this might be evaluated in a constant expression"
  • A constexpr function that never can be used in a constant expression is ill-formed:
  • But it's the worst kind of ill-formed: 😱 no diagnostic required 😱
  • Templates are the same way with respect to template parameters
  • Why is this IFNDR?
    • We don't want to force compilers to solve "hard problems"

IFNDR, in a nutshell

Why are we allowed to assert in a constexpr function?

  • Remember this sometimes constexpr function from the earlier slide?
  • The assert macro is usually written something like this:
  • So assert(expr()) is only a constant expression if expr() evaluates to true
    • But that's exactly what we want! We want our constant evaluated code to not compile if the assertion fails!

What does constexpr really mean?

From P2448 by @BarryRevzin


"constexpr functions and function templates in C++ generally speaking mean maybe constexpr. Not all instantiations or evaluations must be invocable at compile time, it's just that there must be at least one set of function arguments in at least one instantiation that works."



...but this might change soon!

Trick 2: Using operator[] with a Tuple

Making literals into types

Cute C++ trick of the day: ever wished you could index into a tuple using operator[]? You can do that with numeric literal operator templates!

Step back: C++ literals

  • Integer literals
    • Examples: 42, 0x5A6DF3, 123ull, -123'456'789ll
  • Floating-point literals
    • Examples: 42.0, 42., 4.2e1, 0xa.bp10f, 0X0p-1
  • String literals
    • Examples: "hello", L"ABC", R"foo(hello world)foo"
  • Character literals
    • Examples: 'a', U'🍌', '\n'
  • Boolean literals
    • Examples: true, false
  • "The Pointer Literal"
    • Examples: nullptr

Step back: C++ literal suffixes

  • Literals are prvalue expressions with a type determined by the compiler based on value and syntax
  • Suffixes can be used to force a literal to have a particular type
    • 25u or 17U is a unsigned int (larger values will have a larger type)
    • 25l or 17L is a long int (or larger for larger values)
    • New in C++23: 25uz or 17UZ is a std::size_t
    • New in C++23: 25z or 17Z is a std::make_signed_t<std::size_t>

User-defined C++ literals



Standard library user-defined C++ literals

User-defined C++ literal templates

  • Since C++11, you can define a user-defined literal operator template of the form
  • The main intended use seems to be multi-precision integer libraries
  • But we can return anything we want from a user-defined literal template...

Turning literals into types (without using angle brackets)

  • In the trick, define an index class template to hold the indices as non-type template parameters:
    (We could have just as easily used std::integral_constant though)
  • Then we basically just use the function from the previous trick to convert the characters to a number:
  • Now we have the index in a template parameter and we can just use std::get()

Trick 3: Jumping over unreachable code

Storing and loading values by jumping over unreachable code?

Cute C++ trick of the day: a few odd quirks of C++ allow you to create a registry of atomically-modified global variables that spookily jump over unreachable code

A simplified version


What's going on?

  • The set() function passes the value to impl() and ignored the value that it expects impl() to throw
  • When called, impl() stores its argument in a static variable during the initializer of another static variable _ and throws before that initializer can return
  • We used the comma operator for compactness, but we could have written this with an immediately evaluated lambda instead:

What happens when a static block-local initializer throws?

  • C++ guarantees that static local initializers will be run exclusively by the first thread that reaches the initializer
  • If other threads reach the declaration before the initializer finishes, the compiler guarantees they'll wait
  • If the evaluation of the initializer throws, C++ guarantees that the next thread that reaches the initializer will try to initialize the static variable.
  • So this impl() always runs stored = v; in a thread-safe way every time it is called, since the initializer of _ never completes without an exception.

How are we getting the value back?

  • Line 4 is unreachable because line 3 always throws
  • But line 5 still determines the return type of impl().
  • The type of every lambda is unique, and lambdas that don't capture are guaranteed to be default constructible
  • static variables are not part of lambda capture. The reference to stored is part of the type of the lambda on line 5 (rather than any particular instance).
  • Calling get() effectively executes the body of the lambda in line 5 without ever executing line 4 👻

Using Template Parameters as Keys

  • In the original code, we used a template with a non-type template parameter to allow us to store and load multiple different values this way.
  • There is a unique version of impl() for each template parameter—and thus a unique instance of stored and a unique lambda type for the return
  • set() and get() refer to the version of impl() associated with a given template parameter


Questions?


Thanks for listening!


Daisy Hollman, Google
Twitter: @The_Whole_Daisy
Email: cpp@dsh.fyi