C++ std::function vs Function Pointers: Flexibility vs Speed
이 글의 핵심
std::function vs raw function pointers: pointers are faster and smaller; std::function type-erases lambdas with captures and functors. Callback design, SBO, and when to template instead.
For encapsulating requests as callable objects (undo queues, jobs), the [Command pattern](/en/blog/cpp-command-pattern/ builds on the same callback ideas.
Introduction: “How should I store callbacks?”
Function pointers are small and fast but cannot carry capturing lambdas. std::function is flexible but has overhead. This article covers:
- Capabilities
- Benchmark trends
- Design patterns
Comparison
| Aspect | Function pointer | std::function |
|---|---|---|
| Capturing lambdas | ❌ No | ✅ Yes |
| Functors | ❌ No | ✅ Yes |
| Size | 8 bytes (pointer) | 32+ bytes (SBO + vtable) |
| Heap allocation | Never | Sometimes (large captures) |
| Speed | Fastest | Slower (type erasure) |
| C interop | ✅ Yes | ❌ No |
Function pointers
Basic usage
int add(int a, int b) {
return a + b;
}
// Function pointer type
int (*funcPtr)(int, int) = add;
// Or with typedef
typedef int (*BinaryOp)(int, int);
BinaryOp op = add;
// Call
int result = funcPtr(10, 20); // 30
Limitations
int x = 5;
auto lambda = [x](int y) { return x + y; }; // Capturing lambda
// ❌ Error: cannot convert capturing lambda to function pointer
int (*ptr)(int) = lambda;
// ✅ Only non-capturing lambdas work
auto lambda2 = [](int y) { return y * 2; };
int (*ptr2)(int) = lambda2; // OK
std::function
Basic usage
#include <functional>
std::function<int(int, int)> func = [](int a, int b) {
return a + b;
};
int result = func(10, 20); // 30
Storing capturing lambdas
int multiplier = 5;
std::function<int(int)> func = [multiplier](int x) {
return x * multiplier; // ✅ OK: captures multiplier
};
int result = func(10); // 50
Storing functors
struct Adder {
int base;
int operator()(int x) const {
return x + base;
}
};
std::function<int(int)> func = Adder{10};
int result = func(5); // 15
Performance benchmarks
Test setup: GCC 13, -O3, 10M calls
| Callable type | Time (ms) | Overhead vs direct |
|---|---|---|
| Direct call | 8 | 1.0× |
| Function pointer | 12 | 1.5× |
| std::function (no capture) | 35 | 4.4× |
| std::function (small capture) | 38 | 4.8× |
| std::function (large capture) | 42 | 5.3× |
| Template parameter | 8 | 1.0× |
Key insight: Templates with auto or type parameters have zero overhead compared to direct calls. |
Small Buffer Optimization (SBO)
std::function uses SBO to avoid heap allocation for small captures:
#include <functional>
#include <iostream>
struct Small {
int x; // 4 bytes
};
struct Large {
char data[100]; // 100 bytes
};
int main() {
// Small: likely uses SBO (no heap allocation)
std::function<void()> f1 = [s = Small{42}]() {
std::cout << s.x << "\n";
};
// Large: likely heap allocation
std::function<void()> f2 = [l = Large{}]() {
std::cout << "Large\n";
};
}
Typical SBO size: 16-32 bytes (implementation-dependent)
Real-world use cases
1. Event system
#include <functional>
#include <vector>
#include <string>
class EventSystem {
using Callback = std::function<void(const std::string&)>;
std::vector<Callback> listeners_;
public:
void subscribe(Callback cb) {
listeners_.push_back(std::move(cb));
}
void notify(const std::string& event) {
for (auto& cb : listeners_) {
cb(event);
}
}
};
// Usage
EventSystem events;
int counter = 0;
events.subscribe([&counter](const std::string& e) {
++counter; // ✅ Capturing lambda works
std::cout << "Event: " << e << "\n";
});
events.notify("user_login");
2. Command pattern with undo
#include <functional>
#include <stack>
class CommandManager {
std::stack<std::function<void()>> undoStack_;
public:
void execute(std::function<void()> action,
std::function<void()> undo) {
action();
undoStack_.push(std::move(undo));
}
void undo() {
if (!undoStack_.empty()) {
undoStack_.top()();
undoStack_.pop();
}
}
};
// Usage
CommandManager mgr;
int value = 10;
mgr.execute(
[&value]() { value += 5; }, // Do
[&value]() { value -= 5; } // Undo
);
3. Strategy pattern
#include <functional>
#include <string>
class Validator {
std::function<bool(const std::string&)> strategy_;
public:
void setStrategy(std::function<bool(const std::string&)> s) {
strategy_ = std::move(s);
}
bool validate(const std::string& input) {
return strategy_ ? strategy_(input) : true;
}
};
// Usage
Validator validator;
// Email validation
validator.setStrategy([](const std::string& s) {
return s.find('@') != std::string::npos;
});
bool valid = validator.validate("test@example.com"); // true
When to use templates instead
Template callback (zero overhead)
template<typename Func>
void process(const std::vector<int>& data, Func callback) {
for (int value : data) {
callback(value); // Inlined, no indirection
}
}
// Usage
process(data, [](int x) { std::cout << x << "\n"; });
Benchmark (1M elements):
- Template version: 45ms
std::functionversion: 180ms Trade-off: Templates increase code size (one instantiation per callable type).
Common mistakes
Mistake 1: Empty std::function
std::function<void()> func;
// ❌ Throws std::bad_function_call
func();
// ✅ Check first
if (func) {
func();
}
Mistake 2: Dangling captures
std::function<int()> createCallback() {
int local = 42;
return [&local]() { return local; }; // ❌ Dangling reference!
}
// ✅ Capture by value
std::function<int()> createCallback() {
int local = 42;
return [local]() { return local; };
}
Mistake 3: Assigning incompatible signature
std::function<int(int)> func;
// ❌ Error: signature mismatch
func = [](int a, int b) { return a + b; };
// ✅ Correct signature
func = [](int a) { return a * 2; };
Mistake 4: Unnecessary std::function
// ❌ Overhead for simple case
void process(std::function<int(int)> func, int x) {
return func(x);
}
// ✅ Template for zero overhead
template<typename Func>
auto process(Func func, int x) {
return func(x);
}
Advanced: Type erasure internals
Simplified std::function implementation
template<typename Signature>
class SimpleFunction;
template<typename R, typename... Args>
class SimpleFunction<R(Args...)> {
struct Concept {
virtual R call(Args...) = 0;
virtual ~Concept() = default;
};
template<typename F>
struct Model : Concept {
F func_;
Model(F f) : func_(std::move(f)) {}
R call(Args... args) override {
return func_(std::forward<Args>(args)...);
}
};
std::unique_ptr<Concept> ptr_;
public:
template<typename F>
SimpleFunction(F f)
: ptr_(std::make_unique<Model<F>>(std::move(f))) {}
R operator()(Args... args) {
return ptr_->call(std::forward<Args>(args)...);
}
};
Compiler support
| Compiler | Function pointers | std::function |
|---|---|---|
| GCC | All versions | 4.5+ (C++11) |
| Clang | All versions | 3.1+ |
| MSVC | All versions | 2010+ |
Related posts
Keywords
std::function, function pointer, callback, type erasure, lambda, C++11, performance, SBO
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. std::function vs raw function pointers: pointers are faster and smaller; std::function type-erases lambdas with captures…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ Command Pattern: Complete Guide | Undo· Redo
- C++ emplace vs push: Performance, Move Semantics, and
- C++ Lambdas: Syntax, Captures, mutable, and Generic Lambdas
Keywords Covered in This Article (Related Search Terms)
This article covers C++, std::function, function pointer, performance, lambda, callback, C++11.