Streaming is available in most browsers,
and in the Developer app.
-
Simplify C++ templates with concepts
Discover how C++20 features can take your C++ code to the next level. We'll introduce concepts and explore how you can use it to find errors faster in your generic C++ code. We'll also discuss the latest enhancements to the constexpr feature and show how you can leverage it to improve your app's performance by evaluating code at compile time.
Resources
Related Videos
WWDC22
-
Download
♪ ♪ Alex: Hello, my name is Alex, and I work on Developer Tools. Today I want to talk to you about the new C++ 20 features supported in Xcode 14. I will specifically focus on how C++ 20 concepts simplify and improve the type safety of generic C++ code. I will demonstrate how to use concepts and will explain how to create your own concepts as well. I will end the talk by listing several other new C++20 features supported in Xcode, and will touch on how some of them can be used to improve the performance of your C++ projects through the power of compile time code evaluation.
Before diving into C++ concepts, let's first go over a quick overview of how to write generic code in C++. Let's say I want to write a function that checks if a number is odd. I can write a function that takes in an 'int' parameter, and it would work with any value that can be represented by the 'int' type. What would happen if I pass in a 64 bit unsigned integer value to it? A concrete function like this one does not behave correctly with 64 bit values, as they get truncated to fit into the 'int' type. To fix this, I can make 'isOdd' a function template. Now that I have a function template, I can pass in the 64 bit unsigned integer value to it. The compiler will now automatically generate a specialization of 'isOdd' that works correctly with the 'uint64_t' type. This is really useful as it means I don't have to write two versions of 'isOdd' that operate on two different types. You can use C++ templates to write generic functions like 'isOdd', and generic container classes as well. Let's take a look at how 'isOdd' can be used. This function is tested with several test cases that I added in my test file. Unfortunately, I have made a mistake in one of my tests. The compiler caught the mistake, but instead of pointing to where I made the mistake, the compiler shows an error inside the 'isOdd' template. It looks like I made a typo and wrote out '1.1' in my test instead of '11'. Because of that, the compiler generates a specialization of 'isOdd' that takes in a 'double' type. Unfortunately, it took me some time to find this typo as Xcode didn't point me to the specific location where 'isOdd' was invoked with incorrect type.
Can the language and the compiler help me find mistakes like this faster? Well, in the current example, the requirements for what types are allowed into 'isOdd' are not specified explicitly. There's only a documentation comment that states that I must call isOdd using integer types. Prior to C++20, C++ programmers did not have a good way to specify template requirements when writing generic C++ code. They often had to resort to documentation comments, specific parameter names, or complicated enable_if checks when specifying template requirements. Well, as you might've heard, C++ 20 introduces a new C++ feature called concepts. You can use concepts to validate template requirements in your generic C++ code. Let's take a look at how concepts can help me validate the types that can be passed into 'isOdd'. First, let's go back to the declaration of 'isOdd'. Currently, I use the 'class' keyword to specify that type ‘T’ that's used by this template can be any type. C++ 20 allows me to use a concept instead of the 'class' keyword to restrict the set of types that this template can be used with. I can use the 'integral' concept provided by the standard library to restrict this 'isOdd' function template only to the built-in integer types. The compiler will not even try to specialize this function template when T does not satisfy this concept. The integral concept is declared in the C++ standard library. So I need to include the concepts header to use it in my code. Now that I added an 'integral' requirement to the type _T_ in the 'isOdd' function template, the compiler is able to provide a much clearer diagnostic that points directly at where I made the mistake in my tests. Turns out, '1.1' is a double, and therefore, it does not satisfy the 'integral' concept. The compiler is able to explain this to me with a clear error message that helps me find and fix this typo much quicker than before. In addition to helping me fix the bug, constraining the type passed to 'isOdd' gives me the peace of mind that all the test cases that I have for is 'isOdd' work only with integer types, and that they're actually testing the intended behavior of the algorithm. You can use concepts to declare the intent for which types your templates are meant to be used with. The compiler will then validate the type requirements before your templates are specialized. Let's take a closer look at how concepts can be used and which core concepts are provided by the C++ standard library. The C++ standard library provides a concepts library. It implements a set of core language concepts that you can use to validate the core behavior of a type. You can access this library by including the concepts header in your code.
I've already shown how I can use the 'integral' concept in my earlier example. Now, let's take a look at the other concepts provided by this library. This library provides a number of useful core language concepts, like concepts that test if a type is one of the built-in types. For instance, the 'floating_point' concept is satisfied by built-in types like 'float' and 'double'. The 'static_assert' shown here validates that this is indeed the case. It also provides a lot of other useful core concepts that check if types are constructible, destructible, convertible, or are they the same as another type. For instance, the 'convertible_to' concept tests if a type can be converted to another type. and the 'move_constructible' concept is satisfied by types that can be constructed directly from another value of the same type. This library also provides several comparison concepts that test if types can be compared to other types. For instance, the 'equality_comparable' concept is satisfied by types that have a valid '==' operator that works with a value of the same type.
In addition to concepts mentioned on this slide, this library provides numerous other core language concepts. It also provides concepts that test if a type can be moved or copied. In addition to that, it also provides concepts that check if a type is some callable object.
Now that we've looked over the concepts provided to us by the C++ standard library, let's take a look at how concepts can be used to constrain templates. Like I've shown earlier, you can use a concept instead of the class keyword in a template to restrict which types are allowed for this template. In addition to that, you can use a 'requires' clause in a template declaration if you ever need to constrain a type to multiple concepts. Let's take a look at a slightly different example to see how it can be done.
Here I have 'isDefaultValue' function template. It returns true if the given value is equal to the default value of its type. I can use two concepts from the standard library to test that this type supports these operations before this template is specialized. I'm going to add the 'requires' clause to restrict the set of types which are allowed for this function template. Let's see which concepts from the concepts library can help me validate the type here. First, the 'equality_comparable' concept tests if _T_ can be compared to another value of the same type. Then, the 'default_constructible' concept tests if _T_ is a type with a default constructor. The logical and operator between them instructs the compiler to validate both concepts. This ensures that this function template will only be specialized with supported types. Let's go over what we've learned so far about concepts. You should use concepts to restrict the types which are allowed to be used in your templates. The compiler will then be able to show clearer diagnostics as the template won't have to be specialized if a type mismatch occurs. You should reuse the concepts from the concepts library if you need to validate some core behavior of a type.
You should add the 'requires' clause to your templates when you need to test if types conform to multiple requirements. We've now seen how to use concepts in C++ programs. C++ allows us to declare custom concepts that validate specific behavior of a type. Let's take a look at how to create our own concepts that validate specific type behavior. Before we do that, though, we need to take a look at how to identify the behavioral requirements that must be validated by the concept we want to declare. I'm going to use a new example to illustrate how to validate specific type behavior using concepts. Say I am building a C++ library that can render various two-dimensional shapes to an image.
I would like to support various shapes in my library. I'm starting out with a circle shape, as it's the simplest to render. I'm going to use a C++ class to store its properties, like position and radius. In order to render the circle, I'm going to use a distance-function based rendering algorithm that runs on each pixel in the rendered image. This algorithm needs to compute the distance to the shape's surface in order to render it. The 'getDistanceFrom' method in the Circle class computes it. It returns a negative distance inside the circle, and a positive distance outside the circle. In addition to the circle, I would like to render other shapes. For instance, by geometrically subtracting one circle shape from another circle shape, I can render a crescent shape as well. I'm going to represent shapes like Crescent that I would like to render using classes as well. Each new shape class includes the 'getDistanceFrom' method. After creating several shape classes, I now would like to try rendering these shapes to verify their implementation. I have a couple of options for how I can create the rendering function that works with any shape. I can create a class hierarchy for the shapes, and use a virtual method to compute the distance to the shape's surface. However, I'm going to use a function template instead for performance reasons, as I want to avoid the virtual call overhead as this function is going to be called millions of times during rendering. This is why I created this rendering function template. The computePixelColor function takes in a shape value, and checks if the given pixel is inside the shape. If it's inside, it returns a plain white color. This now allows me to verify that shapes can be filled in correctly.
This function is a template, which makes it work with any shape type, be it a circle, crescent, or any other matching type. Even though a template works well here, I would like to use concepts to constrain the type that can be passed to this function. Constraining the type that's passed to this function will allow the compiler to produce clearer diagnostics when a type mismatch occurs. In addition to that, constraining the type that's passed to this function will also allow me to add additional overloads of this function.
In order to constrain the type, I'm going to create a Shape concept. This concept will validate the type's behavior, and will accept classes like circle, crescent, and any other shape class that I might want to add in the future. In order to create a concept like 'Shape', I first need to identify the requirements that must be validated by this concept. Let's see how this can be done. This function template uses type 'T' as the generic type. An argument named 'shape' of type 'T' is then passed to this function. The 'shape' argument is then used inside the function, when I call the 'getDistanceFrom' method on it. As you can see, this is the only requirement I want to validate in my concept, as no other operations are being performed on shape in this function.
You can use the 'requires' expression to test if a type behaves in a specific manner. Let's take a look at how I can use 'requires' to create the Shape concept. I need to provide a set of expressions that test the behavior of a type inside the 'requires'. I already identified the call to 'getDistanceFrom' as a single requirement I need to test, so now I can go ahead and create the 'Shape' concept. I declared the shape concept using the 'concept' keyword. I then added the 'requires' expression to this concept to validate the type. I added an argument list to the 'requires' expression. This argument list allows me to declare a value 'shape' of type 'T' that I will then be testing inside the 'requires'. You can use an argument list in a requires expression to declare values of any type. You will then be able to use these values inside the requires. The body of the 'requires' expression contains a set of requirements that must pass in order for this concept to be satisfied. The 'shape' concept has just one simple expression requirement that checks whether a method call to 'getDistanceFrom' is valid. This expression isn't actually going to be executed in the program. It's only needed at compile time to validate the type's behavior, and it's discarded after the validation. You can use expression requirements to validate the type's behavior by testing if a particular expression compiles or not. This particular expression is not yet complete though, as we're missing the arguments to the 'getDistanceFrom' method call. I know that I want this method to take two values of type 'float', so I can use two floating point literals to complete this expression. I am going to add an additional check to test that 'getDistanceFrom' method returns a float value, as that's what is being assumed by my generic code. I'm currently using a simple expression requirement to test if the type has the 'getDistanceFrom' method. However, I can use a compound requirement instead of the expression requirement to test that it returns a float value. The arrow operator can follow a compound requirement. The arrow operator expects a constraint on its right hand side, so this is where I can use a standard library concept like 'same_as' to validate that the call to 'getDistanceFrom' method returns a float value. Now this concept looks ready to me.
I can go ahead and use it to constrain the types that can be passed to my 'computePixelColor' function. Now my generic 'computePixelColor' function will only work with types that satisfy the 'Shape' concept. This means that classes like Circle and Crescent will be rendered using this particular generic 'computePixelColor' function, as both of these types satisfy the 'Shape' concept.
After seeing the plain shapes rendered, I would like to create a different version of 'computePixelColor' that adds colors to some of my shapes. Let's say I want to add a colorful GradientCircle class to my shape library. I now need a new function to compute the pixel color in the image. C++20 allows me to create multiple variants of the 'computePixelColor' function template. Each variant must be constrained using different concepts. I am going to create a new GradientShape concept that will be satisfied by classes like GradientCircle. This concept will then constrain a new variant of 'computePixelColor' that only works with shapes that have a gradient. This concept is implemented using a 'requires' expression, just like the Shape concept. However, since I want GradientShape to satisfy the original Shape concept as well, I include it as the first requirement in the new concept. This ensures that a class that satisfies the GradientShape concept also satisfies the Shape concept, which means I can still call the 'getDistanceFrom' method for values of such class. I then use the logical and operator and the 'requires' expression to ensure that the GradientShape concept can only be satisfied by classes that have the 'getGradientColor' method. Now that I have created the GradientShape concept, I can go ahead and create a new variant of 'computePixelColor'. This function template only works with shape classes with a gradient, like the GradientCircle class, as it is constrained by the GradientShape concept. Now that I have all the pieces in place, I can go ahead and try rendering a circle with a gradient. Here I'm rendering a GradientCircle. Let's see which overload of 'computePixelColor' the compiler is going to pick inside the 'render' function.
Even though GradientCircle can be safely used with both variants of computePixelColor, the compiler picks the overload that is constrained with the GradientShape concept as it's more specific than the first overload. Because the compiler picks the most matching overload of 'computePixelColor', I can see this beautiful gradient circle rendered when I test my library. Amazing! Now let's go over what we've learned about creating concepts.
You can create concepts by identifying the behavioral requirements in your existing generic code. You should use the requires expression to create concepts to validate the behavior of types. You can also use concepts to create more specific variants of generic functions and classes.
We've now seen how to enhance your generic C++ code with concepts. In addition to supporting concepts, Xcode 14 has improved its support for other C++20 features as well. More specifically, I would like to highlight the improved support for compile-time C++ code evaluation in Xcode 14.
Compile time code evaluation is useful as it can reduce the cost of initialization for variables in your C++ code. This could help reduce your app launch time if your app has a lot of C++ code that depends on complex initialization sequences. In addition to that, compile time code evaluation can help you validate constants that require validation at compile time. This could help you catch bugs before your code even runs. Let's take a look at an example to see how I can use compile time code evaluation in C++.
Here I have a snippet of code that initializes a color palette in my shape rendering library. This library is then used in an iOS app that renders the shapes to the display. Each color in the palette is initialized by parsing a string literal with the HTML hex code of the color. Currently, the 'fromHexCode' function needs to parse three string literals during the initialization of the array. Complicated constant initialization operations like this one can have a measurable impact on the launch time of my app if I have a lot of them. I can use compile-time code evaluation to ensure that this array is initialized with constant color values instead. Let me show you can this can be done. The 'constexpr' keyword enables compile-time code evaluation in C++. I must add it in several places in my example in order to ensure that palette is a constant color array. First, I need to add the 'constexpr' keyword to the 'fromHexCode' function. The compiler will be now be able to execute the code in this function at compile time when it's used in a compile time initialization sequence. You should make your C++ functions 'constexpr' when you want them to be evaluatable at compile time. The compiler will let you know if the code in such function cannot be evaluated at compile time by showing an error when you use it in a 'constexpr' initialization sequence. However, you can also examine a function before adding 'constexpr' to see if it can be evaluated at compile time. Let's take a peek into fromHexCode to see how to check if a function like this one can be a good candidate for compile time code evaluation. This function uses a number of language constructs like if statements, and primitive operations like comparison operators and arithmetic operators. All of these operations can be evaluated at compile time. Also, this function makes several calls to another function; hexToInt. I have already annotated hexToInt function with 'constexpr', so calls to this function can be evaluated at compile time. Overall, it looks like fromHexCode contains code that the compiler should be able to evaluate at compile time, so I think it's safe to proceed and use it in a compile time initialization sequence. After making sure that fromHexCode can be evaluated at compile time, I then need to add the 'constexpr' keyword to the 'colorPalette' variable declaration. The compiler now guarantees that it will evaluate the entire initialization sequence for this array at compile time. More specifically, the compiler will evaluate each call to the fromHexCode function. The evaluation will produce a constant color value that will replace the original call to the function in the palette's initializer. Since all the calls to fromHexCode are now replaced by constant color values, the 'colorPalette' variable is now guaranteed to be initialized by an array literal that contains constant color values. This means that now my app doesn't have to pay additional cost for parsing the color values when this palette is initialized. This is great for the launch time of my app, as it reduces the amount of work this C++ library inside the app has to do at startup. You should make your C++ variables 'constexpr' when you want to ensure that they are initialized with constant values. Xcode 14 has actually greatly improved its standard library support for compile time evaluation. This year we've added the 'constexpr' support to several different standard library types and algorithms, which can now be used during compile-time code evaluation.
In addition to that, Xcode 14 has greatly improved its C++20 standard support. All of the features shown here can now be used in C++ 20 mode.
You should switch to C++ 20 mode today if you haven't already done so. You can use the "C++ Language Dialect" setting in your Xcode project to upgrade to C++ 20. Switching to C++20 will let you use features like concepts in your code. C++20 does not require a minimum deployment target, so you can still ship your code for the same OS version that you're currently targeting. Try C++20 today. Thank you! Enjoy the rest of the developer's conference.
-
-
0:02 - snippet1
int main() { }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.