What you can learn from being too cute

Why you should write code that you should never write

(Part 1 of N)

Daisy Hollman, Google

CppCon 2021

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
    • It's been a long week; we could all use a laugh
    • 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

Who am I?

Qualifications

Who am I?

Qualifications

Who am I?

Qualifications

I agree with Daisy on this one


Titus Winters, C++ committee mailing list (and again at lunch on Tuesday)

Who am I?

Qualifications

+1


Bjarne Stroustrup, C++ committee mailing list

Criticism

I hate this talk.


Bjarne Stroustrup (paraphrased), CppCon 2021 Keynote

Criticism

This is nuclear weapons.


Titus Winters, about a C++ library design I proposed


Anyway...

Here we go!

🤷🏼‍♀️ 🌱 🌼

Trick 1: Iterate Backwards Through a Parameter Pack

Iterate backwards?

"You can iterate backwards through a parameter pack by folding over a right-associative operator (like assignment, for instance)"

(well, it's not the associativity itself, it turns out)

Reactions

But why?

First, without parameter packs:

More Reactions

Operator Order of Evaluation

  • [intro.execution]/10 "Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. The value computations of the operands of an operator are sequenced before the value computation of the result of the operator.";
  • [expr.comma]/1 "A pair of expressions separated by a comma is evaluated left-to-right; the left expression is a discarded-value expression. The left expression is sequenced before the right expression
  • [expr.ass]/1 "In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. The right operand is sequenced before the left operand.

Fold expressions

Fold expressions, comma operator

A deeper dive on order of evaluation

Trick 2: Execute Once Lambda

Execute Once?

"If you want some block of code to execute exactly once in the lifetime of your program, even in the presence of threads, you can put it in an immediately evaluated lambda with a (potentially unused) static local variable."

int bar = 0;
void foo() {
  static bool unused = []{
    bar += 1;
    return true;
  }();
}
foo():
        movzx   eax, BYTE PTR guard variable for foo()::unused[rip]
        test    al, al
        je      .L13
        ret
.L13:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:guard variable for foo()::unused
        call    __cxa_guard_acquire
        test    eax, eax
        jne     .L14
        add     rsp, 8
        ret
.L14:
        add     DWORD PTR bar[rip], 1
        mov     edi, OFFSET FLAT:guard variable for foo()::unused
        add     rsp, 8
        jmp     __cxa_guard_release
bar:
        .zero   4

Static Block Variables

  • [stmt.dcl/4] "Dynamic initialization of a block variable with static storage duration or thread storage duration is performed the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization."
  • Wait, "If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration"??!? That gives me a really bad idea...

Bonus Trick: Execute N Times Lambda

std::execute_thrice?

"If you want some block of code to execute exactly N times in the lifetime of your program, you can use the execute once lambda trick (that I posted a while back) but exit the lambda via an exception that you immediately catch"

Exclusive Execution!

"If you want some block of code to always execute exclusively, you can use a static block-local immediately evaluated lambda that always exits via an exception (that immediately gets caught and ignored)"

What should you actually do?

Execute once semantics in "real" code

What should you actually do?

Exclusive execution semantics in "real" code

What should you actually do?

Execute (exclusively) the first N times in "real" code

Trick 3: Enumerating Parameter Packs

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

Enumerating a Parameter Pack

Iterating over, enumerating, and indexing into parameter packs are perhaps the tasks with the greatest discrepancy between how difficult they should be and how difficult they actually are (but C++20 makes it substantially easier!)

Indexing into Parameter Pack

Note: This topic has been explored in far, far more detail than I have time for here by Louis Dionne: ldionne.com/2015/11/29/efficient-parameter-pack-indexing.I'm going to focus on the cute" ways of doing it.


Trick 4: Getting the last parameter in a pack

The Last (Element in a Pack) Frontier

"Here's an adorable way to get a name for the last parameter in a pack (or really, for any subset of the pack)"

Why can't we have nice things?

Why can't we have nice things?

??!??!?


Everyone on the C++ committee that I've asked about this.

Trick 5: Deduction Guides for Aggregates

Warning: Not actually too cute!

Deduction guides on aggregates?

Cute C++ trick of the day: C++17 deduction guides and class template argument deduction make it easier than ever to use the "rule of zero" for constructors, even for classes with relatively specific template parameters to deduce

An innocent question

Wouldn't it be nice if we could just omit the deduction guide?

  • Who thinks we should add this to the C++ standard?
  • Who thinks I'm asking leading questions about something that's already in the standard?

Thanks Timur!

This works fine in C++20 as long as __cpp_deduction_guides >= 201907

Advanced deduction guides on aggregates!

Cute C++ trick of the day: C++17 deduction guides and class template argument deduction make it easier than ever to use the "rule of zero" for constructors, even for classes with relatively specific template parameters to deduce

Question: Designated initializers?


Fails to compile in Clang!

(But clang doesn't set __cpp_deduction_guides >= 201907, so it's okay?)

Oops

Designated initializers with CTAD (but without deduction guides)

This should work in C++20!

Designated initializers with CTAD (but without deduction guides)

  • [over.match.class.deduct]/1.4.2 In addition, if C is defined and its definition satisfies the conditions for an aggregate class ([dcl.init.aggr]) with the assumption that any dependent base class has no virtual functions and no virtual base classes, and the initializer is a non-empty braced-init-list or parenthesized expression-list, and there are no deduction-guides for C, the set contains an additional function template, called the aggregate deduction candidate, defined as follows. Let x_1, ..., x_n be the elements of the initializer-list or designated-initializer-list of the braced-init-list, or of the expression-list. [...]
  • Basically, the CTAD mechanism performs initialization and overload resolution on the set of deduction guides. The standard doesn't say what happens (with respect to overload resolution) if the arguments are designated initializers.

Once we clarify, should this work with deduction guides or not?




(Probably "or not". Short version: overload resolution is hard, and incorporating designated initializers might actually make it impossible.)


(But this is ongoing work as of this morning)