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("[email protected]"); // 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
- [C++ Lambda complete guide](/en/blog/cpp-lambda-complete/
- [Command pattern](/en/blog/cpp-command-pattern/
- Performance optimization
Keywords
std::function, function pointer, callback, type erasure, lambda, C++11, performance, SBO
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. std::function vs raw function pointers: pointers are faster and smaller; std::function type-erases lambdas with captures… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ Command Pattern: Complete Guide | Undo· Redo](/en/blog/cpp-command-pattern/
- [C++ emplace vs push: Performance, Move Semantics, and](/en/blog/cpp-comparison-11-emplace-push/
- [C++ Lambdas: Syntax, Captures, mutable, and Generic Lambdas](/en/blog/cpp-lambda-complete/
이 글에서 다루는 키워드 (관련 검색어)
C++, std::function, function pointer, performance, lambda, callback, C++11 등으로 검색하시면 이 글이 도움이 됩니다.