WWDC Quick Look 💓 By SwiftGGTeam
Simplify C++ templates with concepts

Simplify C++ templates with concepts

Watch original video

Highlight

C++20 Concepts allows the error information of generic code to be transferred from inside the template to the calling site, and the compiler rejects unmatched types before instantiation; with constexpr compile-time evaluation, the runtime initialization overhead can also be advanced to the compilation stage.

Core Content

Template error reporting is a headache

When writing C++ templates, a common scenario: functionsisOddThe original intention was to only accept integers, but the test code was written by hand.1.1instead of11. The compiler error points to the inside of the template, saying%operator cannot be used withdouble. Developers have to rummage through layers of template expansion information to find the real error at the call site.

Before C++20, template parameters could only be constrained by comments, parameter naming conventions, or complexenable_if. These methods either have no compiler checks, or the error messages are still obscure.

Concepts: Intercept errors before instantiation

C++20 introduces Concepts, which allows the conditions that parameter types must meet to be specified when the template is declared. The compiler checks the concept before trying to instantiate the template, and if there is no match, an error will be reported directly at the calling site.

Detailed Content

Use standard library Concept to constrain templates

03:56

#include <concepts>

// Before: any type could be passed in
template <class T>
bool isOdd(T value) {
    return value % 2 != 0;
}

// Now: only integer types are accepted
template <std::integral T>
bool isOdd(T value) {
    return value % 2 != 0;
}

// Test code
isOdd(11);        // Passes
isOdd(1.1);       // Compiler error: double does not satisfy the integral concept

Key points:

  • std::integralis a concept provided by the C++ standard library and is included in<concepts>in header file
  • usestd::integralsubstituteclassKeyword, restricts the template to only accept built-in integer types
  • incomingdoubleWhen, the compiler directly reports an error inisOdd(1.1)this line instead of inside the template

Standard Library Concepts Overview

06:04

<concepts>The header files provide a core set of concepts:

#include <concepts>

// Type concepts
static_assert(std::floating_point<float>);      // float satisfies it
static_assert(std::floating_point<double>);     // double satisfies it

// Constructible/destructible
static_assert(std::destructible<int>);
static_assert(std::move_constructible<int>);

// Type conversion
static_assert(std::convertible_to<int, float>); // int can convert to float

// Comparable
static_assert(std::equality_comparable<int>);   // int has the == operator

Key points:

  • floating_point: Test whether it is a floating point type -convertible_to: Test whether type A can be implicitly converted to type B -move_constructible: Test whether the move constructor can be constructed from values of the same type -equality_comparable: Test whether there is a valid==operator

requires clause: multiple conditions constraints

08:22

When a single concept is not enough, userequiresClauses combine multiple conditions:

#include <concepts>

template <typename T>
requires std::equality_comparable<T> && std::default_constructible<T>
bool isDefaultValue(T value) {
    return value == T{};  // Requires == and default construction
}

Key points:

  • requiresclause is placed after the template parameter list
  • use&&To combine multiple concepts, all conditions must be met at the same time
  • This ensures that the template is only instantiated if the type supports the required operation

Custom Concept: use requires expression

13:46

Suppose there is a graphics rendering library with various shapesgetDistanceFrommethod. can define aShape concept:

// Custom Shape concept
template <typename T>
concept Shape = requires(T shape) {
    { shape.getDistanceFrom(0.0f, 0.0f) } -> std::same_as<float>;
};

// Use a concept to constrain the template
template <Shape T>
Color computePixelColor(const T& shape, float x, float y) {
    float distance = shape.getDistanceFrom(x, y);
    return distance < 0 ? Color::white : Color::black;
}

// The Circle class satisfies the Shape concept
class Circle {
public:
    float getDistanceFrom(float x, float y) const {
        float dx = x - centerX;
        float dy = y - centerY;
        return std::sqrt(dx * dx + dy * dy) - radius;
    }
private:
    float centerX, centerY, radius;
};

Key points:

  • conceptKeywords define new concepts -requiresThe argument list in an expression declares the values to be tested
  • Expression requirements{ ... }Test whether a certain piece of code compiles --> std::same_as<float>Check return value type with compound requirement
  • The expression is only verified at compile time and will not be actually executed.

Concept overloading: automatically select the best matching function

18:20

Functions with different concept constraints can form overloads, and the compiler automatically selects the most specific version:

// Base shape concept
template <typename T>
concept Shape = requires(T shape) {
    { shape.getDistanceFrom(0.0f, 0.0f) } -> std::same_as<float>;
};

// Gradient shape concept (also satisfies Shape)
template <typename T>
concept GradientShape = Shape<T> && requires(T shape) {
    { shape.getGradientColor(0.0f, 0.0f) } -> std::same_as<Color>;
};

// Base version: all shapes
template <Shape T>
Color computePixelColor(const T& shape, float x, float y) {
    return shape.getDistanceFrom(x, y) < 0 ? Color::white : Color::black;
}

// Gradient version: shapes with gradients
template <GradientShape T>
Color computePixelColor(const T& shape, float x, float y) {
    float dist = shape.getDistanceFrom(x, y);
    return dist < 0 ? shape.getGradientColor(x, y) : Color::black;
}

// GradientCircle satisfies both Shape and GradientShape
// The compiler chooses the second, more specific overload
GradientCircle circle;
auto color = computePixelColor(circle, 1.0f, 2.0f);

Key points:

  • GradientShapeinheritShaperequirements to ensure that they are metGradientShapeThe type must satisfyShape- The compiler chooses the most specific version of the constraint during overload resolution
  • This is much simpler and more intuitive than the SFINAE technique

constexpr: compile-time evaluation reduces startup overhead

21:09

// Parse colors at compile time
constexpr Color fromHexCode(const char* hex) {
    // Parse the #RRGGBB string
    return Color{hexToInt(hex, 0), hexToInt(hex, 2), hexToInt(hex, 4)};
}

// Initialize the palette at compile time
constexpr Color colorPalette[] = {
    fromHexCode("#FF0000"),  // Compile-time evaluation
    fromHexCode("#00FF00"),
    fromHexCode("#0000FF")
};

Key points:

  • constexprMarked functions can be executed at compile time -constexprVariables are guaranteed to be initialized at compile time
  • Xcode 14 enhances the constexpr support of the standard library, and more types and algorithms can be used at compile time
  • Compile-time initialization reduces runtime overhead when the app starts

Upgrade to C++20

26:29

Xcode 14 fully supports C++20. Change “C++ Language Dialect” in Build Settings to C++20 to use concepts and other features. C++20 has no minimum deployment target requirements and can be released against existing supported OS versions.

Core Takeaways

  • Add concept constraints to existing templates Find the commonly used function templates and class templates in the project, and use the standard library concept or custom concept to add type constraints. Error messages are immediately clear and maintenance costs are reduced.

  • Replace complex SFINAE techniques with concepts If there is in the codeenable_ifvoid_tWait for template metaprogramming techniques to be rewritten using concept. The code is more readable and compilation errors are easier to understand.

  • Change constant initialization at startup to constexpr Check constant arrays, lookup tables, configuration data in the project. If the initialization logic can be done at compile time using constexpr, startup time will be reduced.

  • Define concept system for domain model For example, rendering engine definitionShapeGradientShapeTexturedShapeetc concept. New types can be seamlessly connected as long as they meet the concept, and the interface document is the code.

Comments

GitHub Issues · utterances