C++23: The Small Pearls in the Core Language

The C++23 core language has more to offer than deducing this. Today, I will write about the small pearls.

Literal Suffixes

C++23 provides new integral literal suffixes for (signed) std::size_t:

Literal SuffixTypeExample
z or Zsigned std::size_tauto signed = -2023z;
z/Z and u/Ustd::size_tauto unsigned = 2023uz,
std::size_t (C++17) is an unsigned data type that can hold the maximum size of any type. It is commonly used for array indexing and loop counting.

Let me start with a simple example to see the value of the new suffixes.

Assume you want to iterate through a vector. For optimization reasons, you cache the vectors’ size.

#include <vector>

int main() {

    std::vector<int> v{0, 1, 2, 3};
    for (auto i = 0, s = v.size(); i < s; ++i) {
	    /* use both i and v[i] */
    }
}

When you compile the code, you get the following error message in the Compiler Explorer:

The reason is that auto deduced i to int and s to long unsigned int. Consequentially, making both variables unsigned will also not fix the issue.

#include <vector>

int main() {

    std::vector<int> v{0, 1, 2, 3};
    for (auto i = 0u, s = v.size(); i < s; ++i) {
	    /* use both i and v[i] */
    }
}

Now, the compiler deduces i to unsigned int but s still to long unsigned int. The following screenshot shows the Compiler Explorers error output once more.

Thanks to C++23, the new literal suffix z will fix this issue.

#include <vector>

int main() {

    std::vector<int> v{0, 1, 2, 3};
    for (auto i = 0uz, s = v.size(); i < s; ++i) {
	    /* use both i and v[i] */
    }
}

This example is based on the proposal P0330R8. The proposal has more motivating examples for the new literal suffixes.

if consteval

if consteval behaves like if (std::is_constant_evaluated()) { } but has a few advantages:

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    1. No header <type_traits> is required.
    2. Has a more straightforward syntax like std::is_constant_evaluated.
    3. Can be used to invoke an immediate function.

    Let me add a few words. std::is_constant_evaluated is a C++20 feature that detects if a constexpr function is evaluated during compile time.

    cppreference.com/is_constant_evaluated has an excellent example:

    #include <cmath>
    #include <iostream>
    #include <type_traits>
     
    constexpr double power(double b, int x)
    {
        if (std::is_constant_evaluated() && !(b == 0.0 && x < 0))
        {
            // A constant-evaluation context: Use a constexpr-friendly algorithm.
            if (x == 0)
                return 1.0;
            double r {1.0};
            double p {x > 0 ? b : 1.0 / b};
            for (auto u = unsigned(x > 0 ? x : -x); u != 0; u /= 2)
            {
                if (u & 1)
                    r *= p;
                p *= p;
            }
            return r;
        }
        else
        {
            // Let the code generator figure it out.
            return std::pow(b, double(x));
        }
    }
     
    int main()
    {
        // A constant-expression context
        constexpr double kilo = power(10.0, 3);
        int n = 3;
        // Not a constant expression, because n cannot be converted to an rvalue
        // in a constant-expression context
        // Equivalent to std::pow(10.0, double(n))
        double mucho = power(10.0, n);
     
        std::cout << kilo << " " << mucho << "\n"; // (3)
    }
    

    The function power is constexpr. This means it has the potential to run at compile time. The first call of power causes a compile-time execution because the result is requested at compile-time: constexpr double kilo = power(10.0, 1). On the contrary, the second call can only be performed at run time because the function argument n is no constant expression: double mucho = power(10.0, n).

    Thanks to std::is_constant_evaluated, different code is performed at compile time and run time. At compile time, it’s if branch is performed, and at run time, it’s else branch. Both power calls return 1000.

    An immediate function is a consteval function. A consteval function is a function that can only run at compile time. You can read more about consteval functions in my C++20 post: Two new Keywords in C++20: consteval and constinit.

    Based on consteval if, you can implement std::is_constant_evaluated:

    constexpr bool is_constant_evaluated() {
        if consteval {
            return true;
        } else {
            return false;
        }
    }
    

    auto(x) and auto{x}

    A generic way to obtain a copy of an object in C++ is auto copy = x;. This is fine but has one issue: copy is an lvalue, but you sometimes want a prvalue. prvalue is short for pure rvalue. A pure rvalue is an expression whose evaluation initializes an object. Read Barry’s post: Value Categories in C++17, to learn more about value categories.

    The calls auto(x) and auto{x} cast x into a prvalue as if passing x as a function argument by value. auto(x) and auto{x} perform a decay copy. What?

    I often have the question in my class about what decay means. Therefore, let me elaborate on this. Decay essentially means that some type information is lost when you copy a value. A typical example is a function taking its argument by value. Here are the flavors of decay:

    1. Array-to-Pointer conversion
    2. Function-to-Pointer conversion
    3. Discarding const/volatile qualifiers
    4. Removing references

    The following program shows the four flavors of decay:

    // decay.cpp
    
    void decay(int*, void(*)(int), int, int ) { }      // (5)
    
    void func(int){}                                   // (2)
    
    int main() {
    
        int intArray[5]{1, 2, 3, 4, 5};                // (1)
        const int myInt{5};                            // (3)
        const int& myIntRef = myInt;                   // (4)
    
        decay(intArray, func, myInt, myIntRef);       
    
    }
    

    The function decay (5) requires a pointer to an int, a function pointer, and two ints. The first argument of the function call is an int array (1), the second a function (2), the third a const int (3), and the last is a const int& (4).

    The type-traits library has the function std::decay. This function enables you to apply these decay conversions directly on a type. Accordingly, these are the corresponding decay conversions using std::decay.

    // decayType.cpp
    
    #include <type_traits>
    
    int main() {
         
        // (1)
        // int[5] -> int* 
        static_assert(std::is_same<std::decay<int[5]>::type, int*>::value);             
        
        // (2)
        // void(int) -> void(*)(int)
        static_assert(std::is_same<std::decay<void(int)>::type, void(*)(int)>::value);  
        
        // (3)
        // const int -> int
        static_assert(std::is_same<std::decay<const int>::type, int>::value);           
        
        // (4)
        // const int& -> int
        static_assert(std::is_same<std::decay<const int&>::type, int>::value);          
    
    }
    

    What’s next?

    I’m not done with the small pearls in C++23. In my next post, I will continue my journey with C++23 core language features.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *