Cute C++ Tricks

(Part 2.5 of N)

What you can learn from writing code you should never write


Daisy Hollman, Google

CppCon 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

Confession

This isn't the talk I promised to give…

dsh.fyi/cppcon-2022-godbolt-1

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 will actually talk about how to do the same thing in a less surprising way
    • …and also about why not to do it in the first place
  • I have a problem with my talks getting "too into the weeds"
    • 🤷🏼‍♀️ Sorry, this talk is all weeds 🌱


Anyway...

Here we go!

🤷🏼‍♀️ 🌱 🌼

Cute Trick: Partially specializing a concept

Partially specializing a concept

Cute C++ trick of the day: You can't partially specialize a concept in C++20, but you can use explicit template parameters with a lambda to get a similar effect

dsh.fyi/cppcon-2022-godbolt-2

Quick review: concepts


  • Concepts are a compact way of constraining template parameters to have certain properties
  • Alternative notations:

Quick review: partial specialization


  • You can specialize a template by giving explicit types for its parameters
  • You can partially specialize a template by giving more specific values for some or all of its parameters, but still depending on at least one parameter

Quick review: partial specialization


  • Partial specializations don't have to specialize much:
  • Concepts can be used as part of a partial specialization

Cute mini-trick


  • As long as none of the other partial specializations are concept-constrained, you can shadow the unspecialized implementation using a trivial requires clause
  • But the moment you try to use concepts for any other constraint, you get an ambiguous partial specialization

But...

  • If you use a named concept and use subsumption (or if all of your other partial specializations have constraints beyond a plain requires clause) dsh.fyi/cppcon-2022-godbolt-3

"More specialized"


  • A partial specialization can be more specialized without introducing ambiguity
  • What does this print?

    Prints "B"

  • What does this print?

    Prints "C"

Subsumption (how you expect it to work)


  • Concepts that are more specific can also be used with partial specialization without introducing ambiguity
  • What does this print?

    Prints "C"

  • What does this print?

    Prints "B"

Subsumption?


  • What does more specific mean?
  • What does this print?

    Doesn't compile!

Subsumption must be explicit!


  • What does more specific mean?
  • What does this print?

    Prints "C"

Function template partial specialization?


  • Common error message: "Function template partial specialization is not allowed"

    Doesn't compile!!!

  • Just use overloading instead!
  • What does this print?

    Prints "C"

Subsumption and overloading


  • Subsumption works (basically) the same way with overload resolution as it does with partial specialization!
  • What does this print?

    Prints "C"

Challenge #1


  • Write a class template OddDetector with a single template parameter and a member function print() that prints "Odd" if the class's template parameter is itself a template with an odd number of template arguments and "Nope" otherwise (don't worry about types with non-type template parameters)
  • Examples:

Challenge #1 Possible Answer


  • Examples:
  • Primary template:
  • Partial specialization

Challenge #1B


  • Write a class function template OddDetector with a single template parameter [span.strike and a member function print()] that prints "Odd" if the class's template function's parameter is an instance of a type that is itself a template with an odd number of template arguments and "Nope" otherwise (don't worry about types with non-type template parameters)
  • Examples:

Challenge #1B Possible Answer


  • Examples:

Challenge #2


  • Write a concept OddDetector that constraints types to be templates with an odd number of template parameters
  • We can't use partial specialization (directly)!
  • This doesn't compile:

Challenge #2


  • Write a concept OddDetector that constraints types to be templates with an odd number of template parameters
  • We could use class template partial specialization:

Challenge #2 (Cuter answer)


  • Or we can use a function (and just leave off the "Nope" overload)

Challenge #2 (Cutest answer)

  • Or...we can use a lambda to define the OddDetectorImpl function inline!

General Pattern (that you shouldn't use)


  • If you need partial specialization of a concept, you can use the pattern

More General Pattern (that you definitely shouldn't use)


Even More General Pattern (that you definitely shouldn't use)


  • For actual partial specialization semantics, you need to use overloading

Why you should (basically) never use this pattern


  • "I want a concept for a vector of things that work with the plus operator"
    • Do you really want this?
  • Question: when are you writing code that's so specific that you know the container type is exactly vector but so generic that all you know about the value type is that it's addable?

Two better ways to write this (if you're sure you want to)

  • Just constrain the template parameter directly!
  • Or constrain the value type of the container


Thanks!

(I'll answer questions now, in the hallway, on Discord, or on Twitter!)

Twitter: @The_Whole_Daisy
Email: cpp@dsh.fyi


More Tricks!

And also backup slides in the unlikely event that I have extra time

Cute Trick: Sorting a type_list with std::sort

Sorting a type_list with std::sort

Cute C++ trick of the day: Compile-time programming has come a long way in C++, but can you actually just use `std::sort` to sort a list of types yet? Here's one way
(There are ~5 good tricks in this one snippet; I'll do tweets about the others later.)

Type lists

Parameter packs are the compile-time data structure of choice. As of C++11, C++ has language support for lists of types. We would be foolish to work with anything else.


Eric Niebler, ericniebler.com/2014/11/13/tiny-metaprogramming-library

Type list: Implementation

  • My favorite class template:

    That's it!

  • It's just a way to get back to the "native" C++ compile-time data structure.
  • Why are you doing this?!?
    • Any time you find yourself writing a type list in real code, spend a long time thinking about your life decisions whether it's worth the cost
  • But...
    • Type lists are useful for creating simple playgrounds to learn important things about how C++ works

Progress

Here's the part of the cute trick we've explained so far



Implementing type traits using std::type_identity

  • std::type_identity was added in C++20
  • P0887 proposed this.
    • Even small C++ features take a lot of work
    • Thanks, Timur!

Implementing type traits using std::type_identity

Implementing type traits using std::type_identity

  • Just to reiterate:

    is a metaprogramming idiom that's essentially equivalent to:

  • But...the "shortcut" is more characters?!?
    • The purpose of idioms is not to save on typing!
    • The information communicated by the use of an idiom depends on the reader.

Progress

Here's the part of the cute trick we've explained so far



What is a "metafunction"?

  • By convention, a "metafunction" X with arguments Y and Z is "invoked" by getting the type member like this:
  • The "signature" we want for our type_list sort is:
    • Key is a function object that takes a std::type_identity-wrapped type and returns something that works with operator<()
    • Example use:

Using a lambda for the Key function

  • The Key argument to our sort metafunction is a function object that takes a std::type_identity-wrapped type and returns something that works with operator<()
  • C++20 lambdas in unevaluated contexts make this convenient!
  • "Traditional" (pre-C++20) metaprogramming convention for predicates like this: take a type that has a apply member class template that has a type and/or a value member
    • Example:
    • That is, apply is a "metafunction"
    • Convention comes from Boost MPL

Using a lambda for the Key function

  • Speculation: lambdas that take a std::type_identity-wrapped argument will be the new C++20 convention for metafunction predicates
  • Explicit template parameters for lambdas is a helpful C++20 feature here
    • Alternative:
  • Why do we wrap in std::type_identity?
    • What if T is void?
    • …or int[4]?
    • …or int[]?

Progress

Here's the part of the cute trick we've explained so far



Getting the parameter pack out of the type list

  • How do we get the parameter pack back out of the type_list?
    • It would be nice if we could do something like:
  • But we can use partial specialization to get a similar effect:
  • Why not write sort so that it takes a pack directly?
    • How would we write metafunctions that take multiple lists (e.g. zip)?
    • What would we "return"? (Remember, it can't be a pack!)

Getting the parameter pack out of the type list

  • Quick note: if we need to be more general, we can deduce the "container" template:
  • Soapbox: generic programming vs. metaprogramming
    • Note to self: don't spend too long on this soapbox 😬
  • (Caveat: only be as generic as you need to!)
  • For more on partial specialization, see my CoreC++ 2022 talk

Progress

Here's the part of the cute trick we've explained so far



Implementation strategy

  • We've (finally) reached the point where it's time to implement the "body" of the metafunction
  • General strategy overview:
    • Get an array of the indices for our type list (i.e., "enumerate" Ts...)
    • Use std::sort to sort those indices based on the value of the Key function for the type at each index
    • Get the type at the each index in the sorted array of indices
    • Use those types to form the resulting type_list

Enumerating a parameter pack

  • Use std::integer_sequence!
  • The standard provides some helper alias templates:

Enumerating a parameter pack

  • C++20 explicit template parameters in lambdas make this easy!
  • To some extent, this isn't a "cute trick"; it's the expected use of index_sequence
    • Hence the name index_sequence_for
  • I went into more detail on this in my CppCon 2021 talk

Progress

Here's the part of the cute trick we've explained so far



Initializing with an immediately invoked lambda

  • If we need to initialize a constexpr variable with some logic that spans multiple statements, we can use the immediately invoked lambda idiom to initialize it:
  • There are a bunch of other talks that discuss the immediately invoked lambda idiom
    • I recommend Timur Doumler's "C++ Lambda Idioms" talk from this year's CppCon (I'll add a link here when it goes up on YouTube)

Progress

Here's the part of the cute trick we've explained so far



Sort the indices with std::sort!

  • We can make our indices into a "normal" array using a braced initializer
    • Notice that we don't have to give template parameters to std::array because of class template argument deduction (CTAD).
  • Now we just sort!
    • We can shorten this with std::ranges::sort!
    • …but we still need a custom compare function

Progress

Here's the part of the cute trick we've explained so far



Accessing the Nth element of a pack

  • To go from a index to a type in a pack, you need an at metafunction
  • This is more difficult than it sounds
  • Kris Jusiak's talk at CppCon this year (also at C++Now 2022) goes into much more depth on this
  • My CppCon 2021 talk also touched on a "cute" way to do this
  • For completeness, here's a compact (not cute) way to do it for this trick:
  • or slightly cuter:

Applying at for each sorted index

  • Just like with regular functions, we can apply meta functions for each entry in a pack using the unpack operator "..."
  • Notice that we can expand two different packs in different places! The compiler will only expand unexpanded packs in the outermost ...
  • In our case, we want to use the sorted_indices:
    • Because it's a constexpr variable, we can just use it directly in a template argument

How do two pack expansions interact with each other?

  • Pack expansion is explained in the [temp.variadic] section of the standard
  • Conceptually, more nested parameter packs are expanded before less nested ones.
  • Outer expansions only operate on unexpanded packs

How do two pack expansions interact with each other?

  • Example: if (Ts... = int, double, char) and (Idxs... = 0, 1, 2), then:

    becomes

    Idxs is still an unexpanded pack, so we get

Progress

Here's the part of the cute trick we've explained so far



The Compare function for the sort

  • The compare argument to std::sort is a function object that takes two elements of the range and returns true if the first one is ordered before the second one (and false otherwise)
  • What we'd like to do is something like this:

The Compare function for the sort

  • This is kind of a specific case of the general question: How do I turn (potentially) runtime information like function arguments into compile-time information like non-type template parameters?

The Compare function for the sort

  • What we often do for this in "normal" code (in the absence of a vtable, at least) is something like this:
  • variant is another way we often do this kind of thing:
  • But how do we do these things with a parameter pack?

Applying Key with std::variant

  • A variant of all of the types we might want to pass to the Key function looks like this
  • How can we convert an index argument to a variant that contains std::type_identity<at_t<idx1, Ts...>>{}?
    • Make an array of each possibility!
  • Then use std::visit on the idx1 element of this array to get the result of applying the key function!

The Compare function for the sort

  • The full compare function for std::sort is

Progress

Here's the part of the cute trick we've explained so far



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

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

Cute Trick: 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!