katherinemohr.github.io

A silly little C++ quiz

Published April 20, 2026

Last updated: May 1, 2026 I occasionally shoot myself in the foot think of new questions to add here :)


For all the code snippets, unless otherwise stated,

  1. assume they are being run on an x86_64 machine with g++ and -std=c++23 and
  2. assume all necessary headers have been included

Hint: the code snippets might not all compile.


Show all explanations / Hide all explanations

  1. What is the value of x after the following code is executed?
     int x = 0;
     x = x++;
    
    Answer/Explanation

    0

    First, x++ evaluates to 0 (returns old value).
    Then the increment side effect makes x become 1, but assignment writes the old 0 back, so the final x is 0.

    godbolt link


  2. What is the value of x after the following code is executed?
     int x[2] = {0,1};
     x[0], x[1] = x[1], x[0];
    
    Answer/Explanation

    {0, 1}

    The comma operator executes each expression left-to-right, returning the right most value. x[0] is not executable, x[1] = x[1] is a no-op, and x[0] is not executable, so this does nothing.

    Why doesn’t this get parsed as x[0], x[1] = (x[1], x[0]), so the result would be {0, 0}?

    The assignment operator has higher precedence than the comma operator.


  3. Given the following function display, what is the result of passing each of the following vector<string>s into display? Edit: this question is a bit defunct nowadays, since it looks like this ambiguity is disallowed since GCC 15. If you still want to play along, pretend this is clang.
     void display(const vector<string> &str_vec) {
         cout << str_vec.size() << endl;
         for (const string &str : str_vec) {
             cout << str << endl;
         }
     }
    
        
     vector<string> foo{{"hello", ",", "world"}};
     vector<string> bar{{"hello", "world"}};
     vector<string> baz{{"hello", "hello"}};
        
    
    Answer/Explanation

    There is no string constructor that takes in 3 const char*s as input, so foo is actually constructed as the list ["hello", ",", "world"], and display(foo) results in:

     3
     hello
     ,
     world
    

    display(bar) results in an error because clang tries to call the

     template< class InputIt >
     basic_string( InputIt first, InputIt last,
                   const Allocator& alloc = Allocator() );
    

    constructor with "hello" and "world” as the arguments. These two strings are likely stored in unrelated locations in the code, so when clang tries to traverse from first to last, it causes a runtime error.

    display(baz) results in:

     1
    

    (probably, maybe, depends on the implementation…).

    clang will call the same constructor as above, but this time, "hello" and "hello" should be the same pointer, so when clang iterates between them, it just makes the empty string "".

    godbolt link


  4. What is the output of
     int foo = 1;
     cout << foo["bar"] << endl;
    
    Answer/Explanation

    a

    array_ptr[index] and index[array_ptr] both desugar to just

     *(array_ptr + index)
    

    so either syntax does the same thing, and

     "bar"[1] == "a"
    


  5. What is the output of the following?
     struct Bit {
         int b : 2 = 1;
     };
    
     int main() {
         Bit bit;
         cout << ++bit.b << endl;
     }
    
    Answer/Explanation

    -2

    Here, b is a bit-field with an explicit width of 2 bits. Because b is an int (not unsigned), the possible range of values is [-2, 1], so when we increment b from 1, it overflows back to -2.


  6. What does const modify in the following: the int, or the pointer to the int?
     int const *ptr;
    
    Answer/Explanation

    The rule for const is:

    const modifies what is on its left. Unless there is nothing on its left, in which case it modifies what’s on its right.

    So, the const here modifies int.

    This rule can lead to confusion, which has created two opposing style preferences for how to const your types, known as the “East Const” vs “West Const” style. There are plenty of discussions about this online, but I’ll just link this one.


  7. What is the output of the following? Why?
     void print(int &x) { cout << 1; }
     void print(int &&x) { cout << 2; }
     void print(const int &x) { cout << 3; }
     void print(const int &&x) { cout << 4; }
    
     int main() {
         int a = 3;
         print((const int)a);
         print((const int)1);
         print(4);
     }
    
    Answer/Explanation

    222

    In each case, the argument is temporary, either because the argument is a literal or because of the casting. As such, each argument here is a prvalue.

    A temporary can’t bind to an lvalue reference (aka int &), so they bind to rvalue references instead (aka int &&).

    The const goes missing here becase, as stated in the cpp standard,

    “If a prvalue initially has the type cv T, where T is a cv-unqualified non-class, non-array type, the type of the expression is adjusted to T prior to any further analysis.”

    int is a built-in, so the const can be dropped.

    godbolt link


  8. What is the output of the following? Why?
     void print(string &x) { cout << 1; }
     void print(string &&x) { cout << 2; }
     void print(const string &x) { cout << 3; }
     void print(const string &&x) { cout << 4; }
        
     int main() {
         string a = "foo";
         print((const string)a);
         print((const string)"bar");
         print("baz");
     }
    
    Answer/Explanation

    442

    In each case, the argument is temporary, either because the argument is a literal or because of the casting. As such, each argument here is a prvalue.

    A temporary can’t bind to a non-const lvalue reference (aka int &), so they bind to rvalue references instead (aka int &&).

    Unlike the previous question, string is a class type, so the const doesn’t get dropped.

    godbolt link


  9. Given the empty struct struct foo {}, what is the size of foo?
    Answer/Explanation

    1 byte.

    (Note that in C, this would be 0.)

    From the C++ standard,

    “A class with an empty sequence of members and base class objects is an empty class. Complete objects and member subobjects of an empty class type shall have nonzero size.”

    stack overflow discussion


  10. Given the struct below, what is the size of foo?
     struct foo {
         uint8_t a;
         uint16_t b;
         uint8_t c;
         uint32_t d;
         uint8_t e;
     };
    
    Answer/Explanation

    16 bytes

    I’m actually going to link the godbolt link before the explanation this time, since that might just explain it better than me. (Pro tip: the pahole function in godbolt is super helpful for struct packing!)

    Here is a longer post about structure packing (technically for C, but it also mostly applies to C++), but tl;dr, each member wants to be aligned to its width, so we get the following calculation:

     struct foo {
       // a is 1 byte and takes up byte 0 of the struct
      	uint8_t                    a;
    
       // b is 2 bytes, so it must be aligned to an even byte,
       // and it takes up bytes 2-3 of the struct
      	uint16_t                   b;
    
       // c is 1 byte and takes up byte 4 of the struct
      	uint8_t                    c;
          
      	// d is 4 bytes and must be 4-byte aligned,
       // so it takes up bytes 8-11 of the struct
      	uint32_t                   d;
    
       // e is 1 byte and takes up byte 12 of the struct
      	uint8_t                    e;
          
      	// a struct has the alignment of its widest member, which is 4 here,
       // so we round up from 13 to 16
     };
    


  11. Following up on that, given the same struct below, what is the minimum size it could be? How would you need to reorder the members to achieve that size?
     struct foo {
         uint8_t a;
         uint16_t b;
         uint8_t c;
         uint32_t d;
         uint8_t e;
     };
    
    Answer/Explanation

    12 bytes

    There are several possible reorderings, so here is just one of them.


  12. What is the output of the following code?
     #include <iostream>
    
     void f()
     {
         auto counter = []() {
             static int b = 1;
             return b++;
         };
         std::cout << counter();
     }
    
     int main()
     {
         f();
         f();
     }
    
    Answer/Explanation

    12

    This one is very straightforward. As you’d expect (and hope), static variables in lambda functions are initialized once and persisted.


  13. What is the output of the following code?
     #include <iostream>
    
     struct A {
         A() { std::cout << "A\n"; }
         ~A() { std::cout << "~A\n"; }
     };
    
     int main() {
         std::cout << sizeof new A << std::endl;
     }
    
    Answer/Explanation

    8

    As you’d expect, sizeof (A*) is 8 bytes, but why don’t we see A get constructed and destructed?

    From the trusty C++ standard

    “The sizeof operator yields the number of bytes in the object representation of its operand. The operand is either an expression, which is an unevaluated operand…“

    so new A never actually gets evaluated!


  14. What is the output of the following code?
     #include <iostream>
    
     bool _false() { std::cout << "false"; return false; }
     bool _true() { std::cout << "true"; return true; }
     int bar() { std::cout << "bar"; return 0; }
     char baz() { std::cout << "baz"; return 0; }
    
     int main() {
         return _false() && _true() ? bar() : baz();
     }
    
    Answer/Explanation

    falsebaz

    Another simpler one, just to check that you know the conditional will early exit and ignore _true() since false && anything is just false.


  15. In the following code (assuming no optimizations, ie -O0):
    1. How many times is foo’s parameterized constructor called?
    2. How many times is foo’s copy constructor called?
     struct foo {
         int x;
         // Parameterized constructor
         foo(int _x) : x(_x) {}
         // Copy constructor
         foo(foo &other) : x(other.x) {}
     };
    
     struct bar {
         foo x;
         foo y;
         bar(foo _x, foo _y) : x(_x), y(_y) {}
     };
    
     int main() {
         foo x{1};
         foo y{2};
         bar{x,y};
         return 0;
     }
    
    Answer/Explanation
    1. The parameterized constructor is called twice, for foo x{1} and foo y{2}.
    2. The copy constructor is called four times, to construct bar’s parameters _x and _y and then to construct its member variables x and y.

    With optimizations, none of the constructors would ever be called because all the foo and bar code is dead, but even ignoring that, copy elision will reduce the number of copy constructions to 2 since

    “Since C++17, a prvalue is not materialized until needed, and then it is constructed directly into the storage of its final destination”


  16. Identify each stack and heap allocation in the following:
     #include <memory>
    
     int main() {
         int x = 42;
         int *y = new int(8);
         auto z = std::make_shared<int>(-50);
         int result = x + *y + *z;
         delete y;
         return result;
     }
    
    Answer/Explanation

    Stack allocations:

    • int x
    • int *y (specifically the pointer, not the data)
    • shared_ptr<int> z (specifically the handle)
    • result

    Heap allocations:

    • int(8)
    • int(-50) and the corresponding shared_ptr control block


  17. What does the following print out?
     int x = 010;
     std::cout << x << std::endl;
    
    Answer/Explanation

    8

    C++ treats numbers with preceding 0s as octal, and 10 in octal equals 8 in decimal.


  18. When does each variable get allocated and deallocated?
     int a = 0;
    
     void foo() {
         int b = 1;
         static int c = 2;
         {
             int d = 3;
             static int e = 4;
             const int f = 5;
         }
     }
    
     int main() {
         foo();
         return 0;
     }
    
    Answer/Explanation

    This is a storage duration question!

    a, c, and e have static storage durations, so they are allocated at the start of the program and deallocated at the end of the program. (a isn’t declared static but is one by default by virtue of being global.)

    Everything else (b, d, and f) has automatic storage durations, so they are allocated at the start of their block scope and deallocated at the end of the block scope.


  19. What does the following code evaluate to? Why might we choose to write code this way?
     #include <iostream>
    
     template<typename T>
     auto f(T t) -> decltype(t.foo(), int{}) { return 1; }
    
     template<typename T>
     auto f(T t) -> decltype(t + 1, int{}) { return 2; }
    
     int f(...) { return 3; }
    
     struct A { void foo(); };
     struct B {};
    
     int main() {
         std::cout << f(A{}) << f(B{}) << f("hi");
     }
    
    Answer/Explanation

    132

    This is an example of our good friend SFINAE.

    As implied by the expansion of SFINAE (substitution failure is not an error), if an error occurs while trying to substitute in an argument for some template, the compiler will just ignore that function overload instead of erroring.

    We can use this to our advantage by writing code that intentionally errors when we try to substitute in certain types for template parameters. There are a number of ways to do this, but we use decltype([condition to satisfy], int{}). If the [condition to satisfy] succeeds, the comma operator moves on and returns decltype(int{}), which is the correct output type. And if it errors, the function overload is ignored.

    Here,

    1. the first function overload requires that the type T has a foo member function, which only A has.
    2. the second function overload requires that the type T has an operator+, which only the string "hi" has.
    3. the final function overload accepts everything, so B ends up calling this.

    For fans of C++20 and beyond, this can be done in a much cleaner way with concepts. Here is this code translated to use concepts instead:

     template<typename T>
     concept HasFoo = requires(T t) { t.foo(); };
        
     template<typename T>
     concept CanAddInt = requires(T t) { t + 1; };
    
     template<HasFoo T>
     int f(T t) { return 1; }
        
     template<CanAddInt T>
     int f(T t) { return 2; }
       
     int f(...) { return 3; }