Generic Lambdas (C++14)

Lambda expressions with auto parameters for type-agnostic operations

The Problem Before C++14

Before C++14, lambda parameters required explicit types. This meant creating separate lambdas for operations that were logically identical but worked on different types.

// C++11: Separate lambdas for each type
auto int_square = [](int x) { return x * x; };
auto double_square = [](double x) { return x * x; };
auto float_square = [](float x) { return x * x; };

// Or use a template functor (verbose)
template<typename T>
struct Square {
    T operator()(T x) const { return x * x; }
};

C++14 Solution: Generic Lambdas

C++14 allows auto (and later decltype(auto)) in lambda parameter lists, creating a compile-time polymorphic callable.

// C++14: One lambda works for all arithmetic types
auto square = [](auto x) { return x * x; };

int i = 5;
double d = 3.14;
float f = 2.5f;

std::cout << square(i) << std::endl;   // 25 (int)
std::cout << square(d) << std::endl;   // 9.8596 (double)
std::cout << square(f) << std::endl;   // 6.25 (float)

How It Works

A generic lambda is essentially syntactic sugar for a functor with a templated operator():

// This generic lambda:
auto lambda = [](auto x, auto y) { return x + y; };

// Is roughly equivalent to:
struct UniqueLambdaType {
    template<typename T, typename U>
    auto operator()(T x, U y) const {
        return x + y;
    }
};
auto lambda = UniqueLambdaType{};

The compiler generates a unique closure type with a templated call operator.

Multiple Parameters

Each auto parameter can be independently deduced, allowing heterogeneous arguments.

// Mixed types in the same lambda
auto add = [](auto a, auto b) { return a + b; };

// All of these work:
add(1, 2);           // int + int
add(1.5, 2.5);       // double + double
add(1, 2.5);         // int + double (promotes to double)
add(std::string("Hello, "), "World");  // Concatenates strings

Captures and Generic Lambdas

Generic lambdas work seamlessly with captures:

int offset = 10;

// Capture 'offset' by value, parameter 'x' is generic
auto add_offset = [offset](auto x) {
    return x + offset;
};

std::cout << add_offset(5) << std::endl;      // 15
std::cout << add_offset(3.14) << std::endl;   // 13.14

Practical Use Cases

1. Generic Comparison Function

// Generic less-than comparator
auto less_than = [](const auto& a, const auto& b) {
    return a < b;
};

// Works with any comparable type
std::vector<int> nums = {3, 1, 4, 1, 5};
std::sort(nums.begin(), nums.end(), less_than);

std::vector<std::string> words = {"banana", "apple", "cherry"};
std::sort(words.begin(), words.end(), less_than);

2. Pipeline Operations (C++20 Ranges Preview)

// These generic lambdas compose beautifully with STL algorithms
auto is_positive = [](auto x) { return x > 0; };
auto square = [](auto x) { return x * x; };

std::vector<double> data = {-1.0, 2.0, -3.0, 4.0};

// Works with any range in C++20
// auto result = data | std::views::filter(is_positive)
//                    | std::views::transform(square);

// In C++14/17, use with algorithms:
std::vector<double> positives;
std::copy_if(data.begin(), data.end(), 
             std::back_inserter(positives), is_positive);

3. Generic Visitor Pattern

// Generic visitor for variant (C++17)
auto visitor = [](auto&& value) {
    using T = std::decay_t<decltype(value)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "Integer: " << value << std::endl;
    } else if constexpr (std::is_same_v<T, std::string>) {
        std::cout << "String: " << value << std::endl;
    }
};

std::variant<int, std::string> v = "Hello";
std::visit(visitor, v);

C++20: Template Parameter Lists

C++20 extends generic lambdas with explicit template parameter lists:

// C++20: Explicit template parameters
auto lambda = []<typename T>(T value) {
    return value * 2;
};

// C++20: Template parameter packs
auto tuple_printer = []<typename... Args>(const std::tuple<Args...>& t) {
    std::apply([](auto&&... args) {
        ((std::cout << args << " "), ...);
    }, t);
};

Best Practices

  • Use const auto& for read-only parameters to avoid unnecessary copies
  • Prefer auto&& (forwarding reference) when you need to preserve value category
  • Use concepts (C++20) to constrain generic lambdas when type requirements matter
  • Avoid overusing generic lambdas when specific types would make code clearer
  • Do not mix auto and explicit types in the same lambda (consistency matters)

Exercises

Exercise 1

Create a generic lambda that computes the maximum of two values. Test it with int, double, and std::string.

Exercise 2

Write a generic compose function that takes two lambdas and returns their composition. Use it with generic lambdas.

Exercise 3

Create a generic filter function that works with any container type. Use a generic lambda as the predicate.

Exercise 4

Refactor legacy functor classes to use generic lambdas instead. Measure compile-time impact if possible.