Skip to content
C++

Abbreviated Function Templates with auto Constraints

C++20 introduces a terse syntax for constrained templates: placing a concept name in front of auto in a function parameter constrains that parameter without requiring an explicit template clause. Each auto parameter implicitly introduces a fresh type parameter, and each Concept auto adds a constraint on that type. The result is the most concise form of constrained generic code in C++20.

From unconstrained auto to Concept auto

C++14 introduced generic lambdas with auto parameters — effectively lambda templates. C++20 extends this mechanism to regular functions. A function with an auto parameter is an abbreviated function template: the compiler generates a template parameter for each auto. Prefixing auto with a concept name constrains that implicitly-introduced type parameter to satisfy the concept.

// C++14 generic lambda (auto parameter)
auto print = [](auto value) { std::cout << value; };
// Equivalent to: template<typename T> void print(T value) { ... }

// C++20: abbreviated function template (auto parameter in regular function)
void print(auto value) { std::cout << value; }
// Equivalent to: template<typename T> void print(T value) { ... }

// C++20: constrained abbreviated template (Concept auto)
#include <concepts>
void print(std::integral auto value) { std::cout << "integer: " << value; }
// Equivalent to:
//   template<std::integral T> void print(T value) { std::cout << "integer: " << value; }

// Each auto introduces a SEPARATE type parameter:
void add(auto a, auto b) { return a + b; }
// Equivalent to:
//   template<typename T, typename U> auto add(T a, U b) { return a + b; }
// Note: a and b can be different types. Use explicit template if they must match.

Constraining parameters with concept names

Any concept — including multi-parameter ones — can prefix auto. When a concept has more than one template parameter, the first parameter is filled by the deduced type and the remaining ones must be specified explicitly in angle brackets. This works for both function parameters and return types.

#include <concepts>

// Single-parameter concept — type deduced from argument
void process(std::integral auto n)     { /* n is an integral type */ }
void process(std::floating_point auto x) { /* x is a floating-point type */ }

// Overload resolution: integral call picks first, float picks second
process(42);     // std::integral<int> is satisfied → first
process(3.14);   // std::floating_point<double> satisfied → second

// Two-parameter concept — first param deduced, second explicit
// std::convertible_to<From, To>: deduced type is From, To is explicit
void accept(std::convertible_to<double> auto v) {
    double d = v;   // v is implicitly convertible to double
}
accept(42);     // int → double: OK
accept(3.14f);  // float → double: OK
// accept("hi"); // const char* is not convertible to double: error
// Return type can also be constrained with concept auto
std::integral auto compute() {
    return 42;   // must return an integral type; checked at the return statement
}

// Custom concept
template<typename T>
concept Printable = requires(T t) { std::cout << t; };

void print_all(const std::ranges::range auto& container)
    requires Printable<std::ranges::range_value_t<decltype(container)>>
{
    for (const auto& v : container)
        std::cout << v << ' ';
}

Equivalence with explicit template syntax

The abbreviated form is syntactic sugar — every abbreviated function template is exactly equivalent to an explicit template with a requires clause. The compiler generates the same code either way. Knowing the equivalence helps you read both styles and choose between them: the abbreviated form is terser, the explicit form allows naming the type for use elsewhere in the signature or body.

Single constrained parameter

Abbreviated

void f(std::integral auto n) { }

Explicit template

template<std::integral T>
void f(T n) { }

Two independently-constrained parameters

Abbreviated

void f(std::integral auto a, std::floating_point auto b) { }

Explicit template

template<std::integral T, std::floating_point U>
void f(T a, U b) { }

Same type for both parameters (cannot abbreviate)

Abbreviated

// No abbreviated form — must name the type
template<std::integral T>
void f(T a, T b) { }

Explicit template

template<std::integral T>
void f(T a, T b) { }

Constrained auto in lambdas

The same Concept auto syntax applies to lambda parameters, making it easy to write constrained generic lambdas without spelling out a requires clause. This is particularly useful with algorithms and ranges where lambdas are passed as predicates or projections.

#include <concepts>
#include <algorithm>
#include <vector>

// Constrained lambda: accepts only integral types
auto print_int = [](std::integral auto n) { std::cout << n << ' '; };

print_int(42);     // OK
print_int(3u);     // OK (unsigned is integral)
// print_int(3.0); // error: double is not integral

// In algorithms: type-safe predicate
std::vector<int> v = {1, -2, 3, -4, 5};

auto positive_count = std::ranges::count_if(v,
    [](std::signed_integral auto x) { return x > 0; }
);

// Combining multiple concept constraints in a lambda
auto transform_safely = [](std::convertible_to<double> auto value,
                            std::invocable<double> auto func) {
    return func(static_cast<double>(value));
};

Concept auto for variables and return types

The Concept auto syntax is not limited to function parameters. It can also appear in variable declarations and return type positions. In a variable declaration it constrains what the initializer's type may be — if the deduced type does not satisfy the concept, the program is ill-formed. In a return type it constrains the type the function must return, checked at each return statement.

#include <concepts>

// Variable declaration: the deduced type must satisfy the concept
std::integral auto x = 42;        // int satisfies integral: OK
std::integral auto y = 3.14;      // double is NOT integral: compile error

// Useful for documenting intent at the call site
std::floating_point auto result = std::sqrt(2.0);  // communicates the return type

// Return type constraint
std::integral auto safe_divide(int a, int b) {
    if (b == 0) return 0;  // must return an integral type
    return a / b;           // int / int → int: satisfies integral
}

// In generic code, constraining the auto deduction prevents silent type widening
template<typename Container>
std::ranges::range auto filtered_view(Container& c, auto pred) {
    return c | std::views::filter(pred);   // must return something satisfying range
}

When to use abbreviated vs explicit template syntax

Use abbreviated when you don't need to name the type

If T is only used in the parameter position and not in the return type, body, or other parameters, abbreviated is cleaner.

void process(std::integral auto n) { ... }

Use explicit when two parameters must have the same type

Each auto introduces a distinct type. To require T == U, you must name them: template<typename T> void f(T a, T b).

template<std::integral T> T add(T a, T b) { return a + b; }

Use explicit when the type appears in the return position

If the return type must match a parameter type, abbreviated syntax forces you to deduce it again — use explicit and name T.

template<typename T> T identity(T val) { return val; }

Use concept auto in variables to document intent

std::integral auto x = compute(); is more informative than auto x = compute(); when the concept matters to readers.

std::signed_integral auto offset = calculate_offset();
Sign in to track progress