Generic Lambdas vs Templated Member Functions in C++
In modern C++, both generic lambdas and templated member functions enable compile-time polymorphism and code reuse across multiple data types. While a generic lambda is fundamentally syntactic sugar that generates an anonymous closure type with an unconstrained templated call operator under the hood, a templated member function is an explicit method defined within a user-declared class or struct. This article examines the key technical differences between these two constructs, focusing on underlying implementation, state capture, scoping, overloading, and type identity.
Implementation and Under the Hood Behavior
The core technical distinction lies in their structural definition. A templated member function is explicitly declared as a template inside a class:
struct Worker {
template <typename T>
void process(T value) {
// Implementation
}
};When you define a generic lambda (introduced in C++14 using
auto parameters, or C++20 using explicit template parameter
syntax <typename T>), the compiler automatically
generates a unique, unnameable class known as a closure:
auto lambda = [](auto value) {
// Implementation
};Under the hood, the compiler creates a class equivalent to:
class __Lambda_Unique_Name {
public:
template <typename T>
auto operator()(T value) const {
// Implementation
}
};Therefore, a generic lambda does not replace a templated member
function; rather, it automatically generates a class containing a
templated operator().
State Management and Variable Capture
A traditional templated member function only has access to its
explicit arguments and the members of its enclosing class via the
implicit or explicit this pointer. It cannot capture
variables from local scopes without manually passing them as arguments
or storing them as member variables in the class beforehand.
Generic lambdas feature a capture clause ([ ]), allowing
them to automatically capture variables from the enclosing local scope
by value, by reference, or through generalized initialization captures
([x = std::move(y)]). The compiler translates these
captured variables into member variables within the generated closure
class.
Scoping and Local Definition
Templated member functions must reside within a class definition, which is typically declared at namespace scope. Standard C++ does not permit local classes to define templated member functions inside a function body.
Generic lambdas, however, can be declared locally within any
function, method, or expression. This makes generic lambdas ideal for
passing custom predicates or transformations directly to Standard
Template Library (STL) algorithms like std::transform or
std::sort without cluttering the class or namespace
interface.
Overloading and Explicit Specialization
Templated member functions support full and partial overloads within the same class interface, and they participate naturally in standard overload resolution:
struct Handler {
template <typename T>
void handle(T value);
template <typename T>
void handle(std::vector<T> vec);
};In contrast, each lambda defines a distinct type with a single
operator(). A single generic lambda cannot easily define
multiple disjoint overloads for distinct signatures unless you combine
multiple lambdas using tools like the overloaded pattern
with std::variant:
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;Additionally, while member function templates can be explicitly
specialized (typically at namespace scope), a generic lambda cannot have
individual explicit template specializations defined directly on its
call operator. Instead, type-specific logic inside a generic lambda is
usually handled using compile-time branching via
if constexpr.
Type Identity and Signatures
A templated member function has a known, nameable member function pointer signature:
void (Worker::*ptr)(int) = &Worker::process<int>;A generic lambda has an unnameable, unique type generated by the
compiler. It cannot be converted to a regular function pointer if it
captures any state. Non-capturing generic lambdas can be converted to
function pointers, but only after the template parameter is deduced for
a specific call. To store or pass generic lambdas with dynamic
flexibility, developers must rely on type deduction with
auto, template parameters, or type-erasure wrappers like
std::move_only_function and std::function.