본문으로 건너뛰기 C++ std::function vs Function Pointers: Flexibility vs Speed

C++ std::function vs Function Pointers: Flexibility vs Speed

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

AspectFunction pointerstd::function
Capturing lambdas❌ No✅ Yes
Functors❌ No✅ Yes
Size8 bytes (pointer)32+ bytes (SBO + vtable)
Heap allocationNeverSometimes (large captures)
SpeedFastestSlower (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 typeTime (ms)Overhead vs direct
Direct call81.0×
Function pointer121.5×
std::function (no capture)354.4×
std::function (small capture)384.8×
std::function (large capture)425.3×
Template parameter81.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::function version: 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

CompilerFunction pointersstd::function
GCCAll versions4.5+ (C++11)
ClangAll versions3.1+
MSVCAll versions2010+

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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, std::function, function pointer, performance, lambda, callback, C++11.