Cute C++ Tricks

(Part 2 of N)

What you can learn from writing code you should never write


Daisy Hollman, Google

C++North 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

History of this talk

  • Part 1 of N was given at CppCon 2021
    • ...and then at MeetingC++
    • ...and then I talked about it on CppCast
  • These have been some of my favorite talks to write (and present!), so I'm really glad people seem to like them
  • Some of these tricks have been presented as part of other C++ conferences this year
    • ...but some are new
    • Including one trick that was too cute for Twitter. (Also, it touches the darkest part of the standard I've ever interacted with)

Cute Inclusivity Trick of the Day

  • I've gotten a lot of positive feedback on my "cute tricks" talks
    • "Hey I saw your crazy C++ tricks thing! It was fascinating!"
  • Think about the implications of words you use for people who have been called those words before.
  • Particularly when a word has a negative connotation or stigmatizes a group of people, maybe come up with a better word.
    • (Yes, even if not everyone in a marginalized group is bothered by it. Some people are, and that's enough for me!)
    • There are plenty of other words that mean the same thing:

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: Counting Aggregate Members

Counting Aggregate Members

Cute C++ trick of the day: C++ doesn't have much reflection, but you can figure out how many members are in an aggregate by trying to construct it with objects that are convertible to anything.
(Details of most general version omitted; advance version later)

What's going on?

  • "objects that are convertible to anything"

What's going on?

  • "trying to construct it"
  • How do we repeat something N times?


Choose your adventure...

Mood check: Should we go deeper on this?

  • The full (simplified) of the ConstructibleWithN concept:
  • Examples:
  • The full (simplified) of the ConstructibleWithN concept:
  • More examples:
    • We need some way to exclude copy/move constructors
    • We need some way to make Any convertible to an lvalue reference.
  • We need some way to make Any convertible to an lvalue reference.
    • Conversion to int is ambiguous!
    • Conversion would also be ambiguous, but we have an explicit rule in the standard about how to order things if both include reference bindings: [over.ics.rank]/(3.2.3)

We can keep going...

  • What about move-only types?
    • When building the list of candidates, C++ considers deleted constructors (and then errors or substitution-fails if the candidate is selected).
    • In our case, before it can select a candidate, there's an ambiguity between selecting T& and the copy constructor or T&& and the move constructor.

We can keep going...

  • We can disambiguate by const-qualifying the T& operator, making T&& a better candidate (since const has to be added to Any as part of the conversion sequence).
  • There's no way to make that one work (we need real reflection).

Confused? Me too

Me, on Stack Overflow almost 6 years ago


But wait there's more...

  • We need some way to exclude copy/move constructors
  • Let's make constrained conversion operators!


End of "going deeper" section


(There's a bit more cleverness we're skipping here; see the original tweet for more)

Putting it all together

  • The exact number of members in an aggregate is the number N when we can construct it with N objects but not with N+1:
  • Example

What can we use this for?

Reflecting on aggregates!

  • If we know the number of members in an aggregate, we can destructure it!
    (Perfect forwarding omitted for brevity.)
  • You need overloads for all of the sizes you want to support.
  • Note: You can't use SFINAE because structured bindings have to be part of a statement, not an expression, and we can't make this any terser because you can't expand a parameter pack on the left-hand-side of a structured binding.
  • We can use this to automatically generate serialization and deserialization functions!


Choose your adventure...

Mood check: Should we go deeper on this?


Featuring the darkest corner of the standard that made me want to quit C++...

And then I saw this...

Actual picture of my reaction




"Oh no. Seriously?"

and then...




"There's so much you can do with this!"

and then...




"This is terrible. C++ needs real reflection support."

My response

There's this 2015 blog post...


And WG21's response... (wg21.link/cwg2118)





"CWG agreed that such techniques should be ill-formed, although the mechanism for prohibiting them is as yet undetermined."

The Basics

  • We have a class template flag that declares (but doesn't define) a friend function named get_member_type():
  • We have another class template set_member_type that defines the friend we declared in flag:
  • We've created a recipe for violating the one definition rule (ODR):

Now the gross part

  • Remember our type from earlier?
  • What if we do this:
  • Now set_member_type gets instantiated every time the conversion operator is successfully specialized.
  • And that generates a definition of get_member_type
  • We've effectively created a compile-time map from the pair T and I to the type U
  • Then our ConstructibleWithN concept will inject a definition of get_member_type() for each instantiation of the conversion operator:
  • Now we can define an alias that extracts the Nth constructor argument:

And WG21's response... (wg21.link/cwg2118)





"CWG agreed that such techniques should be ill-formed, although the mechanism for prohibiting them is as yet undetermined."

Trick 2: 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

Trick 3: 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 4: 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()