본문으로 건너뛰기 C++ Init-Capture — C++14 , Move, and unique_ptr Patterns

C++ Init-Capture — C++14 , Move, and unique_ptr Patterns

C++ Init-Capture — C++14 , Move, and unique_ptr Patterns

이 글의 핵심

This article explains the difference between C++11 simple capture and C++14 init-capture ([x = expr]), move capture and passing unique_ptr, practical examples, and common mistakes (lifetime and duplicate names).

What is init-capture?

Since C++14, the lambda capture list may use name = expression. This is init-capture (also called generalized capture). It creates a member with the given name inside the closure object and initializes it with the result of the right-hand expression.

Basic syntax and mechanics

int factor = 10;
auto f = [factor = factor * 2]() { return factor; };  // member factor is initialized to 20
std::cout << f() << std::endl;   // prints: 20
std::cout << factor << std::endl;  // prints: 10 (outer variable unchanged)

How it works internally:

Init-capture makes the compiler generate a closure class roughly like this:

// Approximate closure class generated by the compiler
class __lambda_closure {
private:
    int factor;  // captured member variable

public:
    __lambda_closure(int init_factor) : factor(init_factor) {}

    auto operator()() const {
        return factor;  // use member variable
    }
};

// Actual lambda construction
int factor = 10;
__lambda_closure f(factor * 2);  // factor * 2 is evaluated and passed to the constructor

Important points:

  1. The left-hand name in the capture list (factor) is the closure member name.
  2. The right-hand expression (factor * 2) is evaluated immediately when the lambda is defined.
  3. The outer variable (factor) and the captured member (factor) are different objects (same name, different entities).

Why init-capture matters

In C++11, you could only copy or reference outer names with [x] / [&x]—you could not initialize a capture from an arbitrary expression at capture time (you had to introduce a separate local variable).

Practical pain points:

// Problem 1: want a transformed value (C++11)
int celsius = 25;
int fahrenheit = celsius * 9 / 5 + 32;  // extra local needed
auto print_temp = [fahrenheit]() {
    std::cout << fahrenheit << "°F" << std::endl;
};

// Problem 2: repeated pattern across lambdas
int value = 100;
int doubled = value * 2;
auto f1 = [doubled]() { return doubled; };
int tripled = value * 3;
auto f2 = [tripled]() { return tripled; };

// C++14: more concise
auto print_temp = [f = celsius * 9 / 5 + 32]() {
    std::cout << f << "°F" << std::endl;
};

auto f1 = [doubled = value * 2]() { return doubled; };
auto f2 = [tripled = value * 3]() { return tripled; };

Benefits in practice:

  1. Conciseness: fewer temporaries.
  2. Clear intent: the capture list shows what value is stored.
  3. Less scope pollution: no extra names outside the lambda.
  4. Move semantics: supports move-only types like unique_ptr.

C++11 capture vs C++14 init-capture

C++11: default captures

SyntaxMeaning
[x]Copy outer x
[&x]Reference outer x
[=]Default copy capture
[&]Default reference capture

Limitation: to put only 2 * x into the closure, C++11 needs a temporary variable.

int x = 5;
int doubled = x * 2;
auto f = [doubled]() { return doubled; };

C++14: one line with init-capture

int x = 5;
auto f = [value = x * 2]() { return value; };

The name in the capture list is scoped inside the closure; the expression to the right of = is evaluated when the lambda is defined.

Summary

  • C++11: only direct copy/reference of outer names.
  • C++14: store the result of any expression under a new name (copy, move, or temporary construction).

Move capture patterns

Move-only resources (e.g. unique_ptr, thread, some handles) cannot use copy capture [ptr], and reference capture [&ptr] often causes lifetime bugs. Init-capture moves ownership into the closure.

Basic move capture

auto ptr = std::make_unique<int>(42);
auto work = [p = std::move(ptr)]() {
    // p is a unique_ptr member; outer ptr is empty
    std::cout << "value: " << *p << std::endl;
    return *p;
};

work();  // OK

// Note: ptr is now nullptr
if (ptr == nullptr) {
    std::cout << "ptr was moved and is empty" << std::endl;
}

// Using ptr again is undefined behavior!
// *ptr;  // crash!

What happens inside:

// Conceptual closure class
class __lambda_move {
private:
    std::unique_ptr<int> p;  // holds moved unique_ptr

public:
    __lambda_move(std::unique_ptr<int>&& init_p)
        : p(std::move(init_p)) {}

    auto operator()() const {
        std::cout << "value: " << *p << std::endl;
        return *p;
    }
};

auto ptr = std::make_unique<int>(42);
__lambda_move work(std::move(ptr));  // ownership moves into the closure

Why move? (copy vs reference vs move)

// Problem 1: copy capture — compile error
auto ptr = std::make_unique<int>(42);
// auto bad = [ptr]() { return *ptr; };
// error: unique_ptr copy ctor is deleted

// Problem 2: reference capture — dangling risk
auto make_dangerous_lambda() {
    auto ptr = std::make_unique<int>(42);
    return [&ptr]() { return *ptr; };  // dangerous!
}
// ptr is destroyed when the function returns → dangling reference

// Fix: move capture — safe ownership transfer
auto make_safe_lambda() {
    auto ptr = std::make_unique<int>(42);
    return [p = std::move(ptr)]() { return *p; };  // safe
}
// the lambda owns ptr

Practical scenario: async work

// Bad: reference + async
void bad_async_example() {
    std::vector<int> data = load_large_data();  // e.g. 1GB

    // Danger: data is destroyed when the function returns
    std::async(std::launch::async, [&data]() {
        process_data(data);  // dangling reference!
    });
}  // data destroyed → async task may crash

// Good: move capture
void good_async_example() {
    std::vector<int> data = load_large_data();

    std::async(std::launch::async, [data = std::move(data)]() {
        process_data(data);  // safe
    });
}  // original data is empty but the lambda owns the buffer

Naming: new name vs same name

Pattern 1: new name (often clearer)

auto ptr = std::make_unique<int>(42);
auto work = [p = std::move(ptr)]() {  // p is a new name
    return *p;
};

// Benefits:
// - Outer ptr vs captured p are clearly distinct
// - Reduces mistakes using ptr after the move
// - Easier code review

Pattern 2: same name (requires care)

auto ptr = std::make_unique<int>(42);
auto work = [ptr = std::move(ptr)]() {  // same name ptr
    return *ptr;
};

// Notes:
// - Left ptr: closure member
// - Right ptr: outer variable
// - Shadowing: outer ptr is nullptr after move but names look alike

Practical recommendations:

// 1. Short scopes: prefer a new name
{
    auto ptr = std::make_unique<Resource>();
    auto task = [res = std::move(ptr)]() {
        res->use();
    };
}

// 2. Multiple moves: use a prefix/suffix
{
    auto conn = std::make_unique<Connection>();
    auto cache = std::make_unique<Cache>();

    auto worker = [
        conn_ = std::move(conn),
        cache_ = std::move(cache)
    ]() {
        conn_->query();
        cache_->store();
    };
}

Move capture for large containers

// Example: large vector
std::vector<int> generate_data() {
    std::vector<int> v(10'000'000);  // ~40MB
    std::iota(v.begin(), v.end(), 0);
    return v;
}

void process_in_thread() {
    auto data = generate_data();

    // Bad: copy capture (~40MB copy)
    // std::thread t([data]() {
    //     for (int x : data) { /* ... */ }
    // });

    // Good: move capture (pointer swap only)
    std::thread t([vec = std::move(data)]() {
        for (int x : vec) {
            // use original buffer without an extra copy
        }
    });

    t.detach();
    // data is empty; the thread owns the buffer
}

Performance comparison:

#include <chrono>

void benchmark_copy_vs_move() {
    std::vector<int> large_data(10'000'000);

    // Copy capture (slow)
    auto start = std::chrono::high_resolution_clock::now();
    auto copy_lambda = [data = large_data]() {  // copy
        return data.size();
    };
    auto end = std::chrono::high_resolution_clock::now();
    auto copy_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    // Move capture (fast)
    start = std::chrono::high_resolution_clock::now();
    auto move_lambda = [data = std::move(large_data)]() {  // move
        return data.size();
    };
    end = std::chrono::high_resolution_clock::now();
    auto move_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    std::cout << "copy: " << copy_time.count() << " μs\n";
    std::cout << "move: " << move_time.count() << " μs\n";
    std::cout << "speedup: " << (copy_time.count() / move_time.count()) << "x\n";
}

This is common when passing lambdas to std::async or std::thread so large containers are not copied.


unique_ptr capture in depth

This is the usual pattern when the lambda should own the resource. unique_ptr is not copyable, so you must move it with init-capture.

Basic pattern

class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " constructed\n";
    }

    ~Resource() {
        std::cout << "Resource " << id_ << " destroyed\n";
    }

    void process() {
        std::cout << "Resource " << id_ << " processing\n";
    }

private:
    int id_;
};

void thread_example() {
    auto resource = std::make_unique<Resource>(1);

    std::thread t([res = std::move(resource)]() {
        res->process();
        // res is destroyed when the thread finishes
    });

    t.join();

    if (resource == nullptr) {
        std::cout << "original resource is empty\n";
    }
}

// Output:
// Resource 1 constructed
// Resource 1 processing
// Resource 1 destroyed
// original resource is empty

Pitfall 1: do not use the source after move

void dangerous_pattern() {
    auto ptr = std::make_unique<int>(42);

    auto task = [p = std::move(ptr)]() {
        return *p;
    };

    // Dangerous: ptr was already moved
    // if (*ptr > 0) { }  // undefined behavior

    if (ptr != nullptr) {
        std::cout << *ptr << std::endl;
    } else {
        std::cout << "ptr is empty after move\n";
    }
}

Compiler warnings:

auto ptr = std::make_unique<int>(42);
auto task = [p = std::move(ptr)]() {
    return *p;
};
auto value = *ptr;  // warning: use-after-move

Pitfall 2: the lambda may be move-only

void copy_lambda_issue() {
    auto ptr = std::make_unique<int>(42);

    auto task = [p = std::move(ptr)]() {
        return *p;
    };

    // Error: cannot copy the lambda
    // auto task2 = task;

    auto task2 = std::move(task);  // OK

    // task is now invalid
    // task();  // undefined behavior
}

Interaction with std::function:

#include <functional>

void function_wrapper_issue() {
    auto ptr = std::make_unique<int>(42);

    // Error: std::function requires a copyable callable
    // std::function<int()> fn = [p = std::move(ptr)]() {
    //     return *p;
    // };

    // Fix 1: shared_ptr
    auto shared = std::make_shared<int>(42);
    std::function<int()> fn1 = [p = shared]() {
        return *p;
    };

    // Fix 2: unique_ptr inside shared_ptr
    auto wrapped = std::make_shared<std::unique_ptr<int>>(
        std::make_unique<int>(42)
    );
    std::function<int()> fn2 = [p = wrapped]() {
        return **p;
    };
}

Practical patterns: async work

#include <future>
#include <thread>

void async_pattern() {
    auto data = std::make_unique<std::vector<int>>(1000000);

    auto future = std::async(
        std::launch::async,
        [data = std::move(data)]() {
            return std::accumulate(data->begin(), data->end(), 0LL);
        }
    );

    auto result = future.get();
    std::cout << "sum: " << result << std::endl;
}

void multiple_resources() {
    auto conn = std::make_unique<Connection>();
    auto cache = std::make_unique<Cache>();
    auto logger = std::make_unique<Logger>();

    std::thread worker([
        conn = std::move(conn),
        cache = std::move(cache),
        logger = std::move(logger)
    ]() {
        logger->log("start");
        auto data = conn->fetch();
        cache->store(data);
        logger->log("done");
    });

    worker.detach();
}

auto create_task(bool use_cache) {
    auto cache = use_cache ?
        std::make_unique<Cache>() :
        nullptr;

    return [cache = std::move(cache)]() {
        if (cache) {
            cache->use();
        } else {
            // work without cache
        }
    };
}

unique_ptr vs shared_ptr guidelines

// unique_ptr: exclusive ownership (preferred default)
void use_unique_ptr() {
    auto data = std::make_unique<Data>();

    // Pros:
    // - Minimal overhead (no refcount)
    // - Clear ownership (single owner)
    // - Move-only (accidental copies prevented)

    auto task = [data = std::move(data)]() {
        data->process();
    };

    // The lambda is the only owner
}

// shared_ptr: shared ownership (when you truly need it)
void use_shared_ptr() {
    auto data = std::make_shared<Data>();

    // Pros:
    // - Shareable across several lambdas
    // - Copyable callable (works with std::function)
    // - Last reference drops the object

    auto task1 = [data]() { data->process(); };
    auto task2 = [data]() { data->process(); };

    // Cons:
    // - Refcount overhead (atomics)
    // - Possible cycles
    // - Ownership can become unclear
}

// Rules of thumb:
// 1. Prefer unique_ptr by default
// 2. Use shared_ptr when multiple owners are required
// 3. Use shared_ptr for async sharing across tasks
// 4. Move-only lambdas are OK if you do not need copies
// 5. Need std::function → often shared_ptr

Custom deleters

// File handle example
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::cout << "closing file\n";
            fclose(f);
        }
    }
};

using FilePtr = std::unique_ptr<FILE, FileDeleter>;

void file_lambda_example() {
    FilePtr file(fopen("data.txt", "r"), FileDeleter{});

    auto task = [f = std::move(file)]() {
        if (f) {
            char buffer[256];
            while (fgets(buffer, sizeof(buffer), f.get())) {
                std::cout << buffer;
            }
        }
        // FileDeleter runs when the lambda is destroyed
    };

    std::thread t(std::move(task));
    t.join();
}

// Socket handle example (illustrative)
void socket_lambda_example() {
    struct SocketDeleter {
        void operator()(int* sock) const {
            if (sock && *sock >= 0) {
                close(*sock);
                delete sock;
            }
        }
    };

    std::unique_ptr<int, SocketDeleter> socket(new int(create_socket()));

    auto network_task = [sock = std::move(socket)]() {
        send_data(*sock, "Hello");
        // socket closes when the lambda ends
    };

    std::async(std::launch::async, std::move(network_task));
}

Performance considerations:

// unique_ptr: pointer-sized (8 bytes on 64-bit)
sizeof(std::unique_ptr<int>);  // 8

// shared_ptr: object pointer + control block pointer (often 16 bytes)
sizeof(std::shared_ptr<int>);  // 16

void benchmark_ptr_types() {
    const int iterations = 10'000'000;

    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        auto p = std::make_unique<int>(i);
        auto task = [p = std::move(p)]() {};
    }
    auto end = std::chrono::high_resolution_clock::now();
    auto unique_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        auto p = std::make_shared<int>(i);
        auto task = [p]() {};  // copy → refcount bump
    }
    end = std::chrono::high_resolution_clock::now();
    auto shared_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::cout << "unique_ptr: " << unique_time.count() << "ms\n";
    std::cout << "shared_ptr: " << shared_time.count() << "ms\n";
}

Practical examples

Example 1: database connection and async work

#include <memory>
#include <thread>

class DatabaseConnection {
public:
    DatabaseConnection(const std::string& conn_str)
        : conn_str_(conn_str) {
        std::cout << "DB connection created: " << conn_str_ << std::endl;
    }

    ~DatabaseConnection() {
        std::cout << "DB connection closed: " << conn_str_ << std::endl;
    }

    void execute_query(const std::string& query) {
        std::cout << "running query: " << query << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

private:
    std::string conn_str_;
};

void execute_async_query(
    std::unique_ptr<DatabaseConnection> conn,
    const std::string& query
) {
    std::thread([
        conn = std::move(conn),
        query
    ]() {
        try {
            conn->execute_query(query);
            std::cout << "query finished, handling results...\n";
        } catch (const std::exception& e) {
            std::cerr << "query failed: " << e.what() << std::endl;
        }
    }).detach();
}

void example_db_async() {
    auto conn = std::make_unique<DatabaseConnection>("localhost:5432");

    execute_async_query(
        std::move(conn),
        "SELECT * FROM users WHERE active = true"
    );

    std::cout << "main thread continues...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

Example 2: HTTP-style handlers with captured state

#include <optional>
#include <functional>
#include <unordered_map>

// Stub: replace with your authorization logic
int get_user_level(int user_id) { (void)user_id; return 5; }

class RequestContext {
public:
    std::unordered_map<std::string, std::string> headers;
    std::unordered_map<std::string, std::string> query_params;
    std::string body;

    std::optional<int> get_user_id() const {
        auto it = headers.find("X-User-ID");
        if (it != headers.end()) {
            try {
                return std::stoi(it->second);
            } catch (...) {
                return std::nullopt;
            }
        }
        return std::nullopt;
    }
};

auto make_auth_handler(std::optional<int> required_level) {
    return [level = std::move(required_level)](const RequestContext& ctx) {
        auto user_id = ctx.get_user_id();

        if (!user_id) {
            std::cout << "auth failed: missing user id\n";
            return false;
        }

        if (level) {
            int user_level = get_user_level(*user_id);
            if (user_level < *level) {
                std::cout << "insufficient privilege: need=" << *level
                          << ", have=" << user_level << "\n";
                return false;
            }
        }

        std::cout << "auth ok: user " << *user_id << "\n";
        return true;
    };
}

auto make_rate_limiter(std::optional<int> max_requests) {
    return [
        limit = std::move(max_requests),
        count = 0
    ](const RequestContext& ctx) mutable {
        (void)ctx;
        ++count;

        if (limit && count > *limit) {
            std::cout << "rate limit exceeded: " << count << "/" << *limit << "\n";
            return false;
        }

        std::cout << "request allowed: " << count;
        if (limit) {
            std::cout << "/" << *limit;
        }
        std::cout << "\n";
        return true;
    };
}

void example_http_handlers() {
    auto admin_handler = make_auth_handler(10);
    auto user_handler = make_auth_handler(std::nullopt);
    auto limiter = make_rate_limiter(100);

    RequestContext ctx;
    ctx.headers["X-User-ID"] = "42";

    if (limiter(ctx) && user_handler(ctx)) {
        std::cout << "handling request...\n";
    }
}

Example 3: logging with lazy initialization

#include <fstream>
#include <chrono>
#include <iomanip>

class Logger {
public:
    explicit Logger(std::string filename)
        : file_(std::make_unique<std::ofstream>(filename, std::ios::app)) {
        if (!file_->is_open()) {
            throw std::runtime_error("failed to open log file");
        }
    }

    void log(const std::string& level, const std::string& message) {
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);

        *file_ << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
               << " [" << level << "] " << message << std::endl;
    }

private:
    std::unique_ptr<std::ofstream> file_;
};

auto make_async_logger(std::string filename) {
    return [
        logger = std::make_unique<Logger>(std::move(filename))
    ](const std::string& level, const std::string& message) mutable {
        logger->log(level, message);
    };
}

void example_batch_logging() {
    auto logger = make_async_logger("app.log");
    std::vector<std::thread> workers;

    for (int i = 0; i < 5; ++i) {
        workers.emplace_back([
            logger = std::move(logger),
            worker_id = i
        ]() mutable {
            for (int j = 0; j < 10; ++j) {
                logger("INFO",
                    "Worker " + std::to_string(worker_id) +
                    " - job " + std::to_string(j));
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        });
        logger = make_async_logger("app.log");
    }

    for (auto& t : workers) {
        t.join();
    }
}

Example 4: timer callbacks with captured state

#include <chrono>
#include <functional>

class Timer {
public:
    template<typename Callback>
    void schedule(std::chrono::milliseconds delay, Callback&& callback) {
        std::thread([
            cb = std::forward<Callback>(callback),
            delay
        ]() {
            std::this_thread::sleep_for(delay);
            cb();
        }).detach();
    }
};

struct Config {
    void apply() { std::cout << "Config::apply\n"; }
};

void example_timer_with_resources() {
    Timer timer;

    auto counter_callback = [count = 0]() mutable {
        ++count;
        std::cout << "timer fired " << count << " times\n";
    };

    timer.schedule(std::chrono::milliseconds(100), counter_callback);

    auto resource = std::make_unique<std::vector<int>>(1000);

    timer.schedule(
        std::chrono::milliseconds(200),
        [data = std::move(resource)]() {
            std::cout << "data size: " << data->size() << "\n";
        }
    );

    std::string message = "Hello";
    int retry_count = 0;
    auto config = std::make_unique<Config>();

    timer.schedule(
        std::chrono::seconds(1),
        [
            msg = std::move(message),
            retry = retry_count,
            cfg = std::move(config)
        ]() {
            std::cout << "message: " << msg << "\n";
            std::cout << "retries: " << retry << "\n";
            cfg->apply();
        }
    );

    std::this_thread::sleep_for(std::chrono::seconds(2));
}

Example 5: C++11 vs C++14 migration

// Illustrative: assume load_large_data, preprocess, process, and Config exist in your TU.
void cpp11_style() {
    std::vector<int> v = load_large_data();

    std::vector<int> v_moved = std::move(v);
    auto f = [v_moved]() {
        return v_moved.size();
    };

    auto v_shared = std::make_shared<std::vector<int>>(load_large_data());
    auto g = [v_shared]() {
        return v_shared->size();
    };
}

void cpp14_style() {
    std::vector<int> v = load_large_data();

    auto f = [vec = std::move(v)]() {
        return vec.size();
    };

    auto g = [
        data = preprocess(load_large_data()),
        config = std::make_unique<Config>(),
        timestamp = std::chrono::system_clock::now()
    ]() {
        return process(data, *config, timestamp);
    };
}

class LegacyService {
public:
    // Assume: void process(std::vector<int>&);
    std::function<void()> create_task_cpp11(std::vector<int> data) {
        auto shared_data = std::make_shared<std::vector<int>>(std::move(data));
        return [shared_data]() {
            process(*shared_data);
        };
    }

    auto create_task_cpp14(std::vector<int> data) {
        return [data = std::move(data)]() {
            process(data);
        };
    }
};

Example 6: error handling helpers

#include <system_error>

void risky_operation() { throw std::runtime_error("fail"); }

auto make_error_handler(std::string context) {
    return [
        ctx = std::move(context),
        error_count = 0
    ](const std::exception& e) mutable {
        ++error_count;
        std::cerr << "[" << ctx << "] error #" << error_count
                  << ": " << e.what() << std::endl;

        if (error_count > 3) {
            throw std::runtime_error("error threshold exceeded");
        }
    };
}

auto make_retry_handler(int max_retries) {
    return [
        max = max_retries,
        current = 0,
        backoff = std::chrono::milliseconds(100)
    ]() mutable -> bool {
        if (current >= max) {
            return false;
        }

        ++current;
        std::cout << "retry " << current << "/" << max << "\n";

        std::this_thread::sleep_for(backoff);
        backoff *= 2;

        return true;
    };
}

void example_error_handling() {
    auto error_handler = make_error_handler("DB work");
    auto retry_handler = make_retry_handler(3);

    while (retry_handler()) {
        try {
            risky_operation();
            break;
        } catch (const std::exception& e) {
            error_handler(e);
        }
    }
}

[*this] / [=, *this] (C++17)

In a member function, *this capture stores a copy of the current object inside the lambda. That avoids the classic bug where you only capture this by reference and the object is destroyed before the lambda runs.

struct S {
    int n = 0;
    auto make_lambda() {
        return [*this]() { return n; };  // S is copied into the closure
    }
};

Like init-capture, this is a way to build a value snapshot.


Common mistakes and debugging

Mistake 1: use-after-move on the outer variable

void dangerous_use_after_move() {
    auto ptr = std::make_unique<int>(42);

    auto task = [p = std::move(ptr)]() {
        return *p;
    };

    if (*ptr > 0) {  // undefined behavior: ptr was moved from
        std::cout << "value: " << *ptr << std::endl;
    }

    ptr.reset(new int(100));  // risky / implementation-defined interaction with moved-from state
}

Symptoms: segfault, access violation, AddressSanitizer heap-use-after-free, null dereference in the debugger.

Safer patterns:

void safe_move_pattern() {
    auto ptr = std::make_unique<int>(42);

    auto task = [p = std::move(ptr)]() {
        return *p;
    };

    if (ptr != nullptr) {
        // typically not taken after a full move
    }

    ptr = nullptr;

    #ifdef _DEBUG
    assert(ptr == nullptr);
    #endif
}

Compiler flags (examples):

clang++ -Wconsumed -Wunused-value file.cpp
g++ -Wuse-after-move file.cpp
cl /W4 /analyze file.cpp

Mistake 2: mixing reference capture and init-capture semantics

void reference_vs_init_capture() {
    int counter = 0;

    auto ref_lambda = [&counter]() {
        ++counter;
    };

    ref_lambda();
    std::cout << counter << std::endl;  // 1

    int value = 10;
    auto init_lambda = [val = value]() mutable {
        ++val;
        return val;
    };

    init_lambda();  // 11
    std::cout << value << std::endl;  // 10
}

Trap: “shared counter” with init-capture:

void counter_trap() {
    int total = 0;
    std::vector<std::function<void()>> tasks;

    for (int i = 0; i < 5; ++i) {
        tasks.push_back([total = total]() mutable {
            ++total;
            std::cout << total << " ";
        });
    }

    for (auto& task : tasks) {
        task();  // prints 1 1 1 1 1 — separate members, not one shared counter
    }

    auto shared_total = std::make_shared<int>(0);
    tasks.clear();

    for (int i = 0; i < 5; ++i) {
        tasks.push_back([total = shared_total]() {
            ++(*total);
            std::cout << *total << " ";
        });
    }

    for (auto& task : tasks) {
        task();  // prints 1 2 3 4 5
    }
}

Snapshot vs live view:

void snapshot_confusion() {
    std::vector<int> vec = {1, 2, 3};

    auto lambda = [size = vec.size()]() {
        return size;
    };

    vec.push_back(4);
    vec.push_back(5);

    std::cout << lambda() << std::endl;  // still 3

    auto ref_lambda = [&vec]() {
        return vec.size();
    };

    std::cout << ref_lambda() << std::endl;  // 5
}

Mistake 3: invalid or confusing default capture mixes

void mixed_capture_errors() {
    int x = 10;
    int y = 20;

    // auto bad1 = [=, x = x * 2]() { };  // error: duplicate capture of x

    auto confusing = [=, z = y]() {
        (void)x;
        (void)y;
        (void)z;
    };
}

Clearer style:

void correct_mixed_capture() {
    int x = 10;
    int y = 20;
    std::unique_ptr<int> ptr = std::make_unique<int>(30);

    auto lambda1 = [
        x,
        y_doubled = y * 2,
        p = std::move(ptr)
    ]() {
        (void)x;
        (void)y_doubled;
        (void)p;
    };

    ptr = std::make_unique<int>(40);
    auto lambda2 = [
        &,
        p = std::move(ptr)
    ]() {
        (void)p;
    };
}

Mistake 4: dangling references

auto make_dangerous_lambda() {
    std::vector<int> local_data = {1, 2, 3, 4, 5};
    return [&local_data]() {
        return local_data.size();
    };
}

auto make_dangerous_init_capture() {
    int local = 42;
    return [&ref = local]() {
        return ref;
    };
}

Safer options:

auto make_safe_lambda_value() {
    std::vector<int> local_data = {1, 2, 3, 4, 5};
    return [data = std::move(local_data)]() {
        return data.size();
    };
}

auto make_safe_lambda_shared() {
    auto data = std::make_shared<std::vector<int>>(
        std::vector<int>{1, 2, 3, 4, 5}
    );
    return [data]() {
        return data->size();
    };
}

Mistake 5: missing mutable

void missing_mutable() {
    auto counter_ok = [n = 0]() mutable {
        return ++n;
    };
}

operator() is const unless the lambda is mutable, so mutating copy-captured members requires mutable. That does not make the closure thread-safe.

Mistake 6: hidden copy cost

void expensive_copy_mistake() {
    std::vector<int> large_data(1'000'000);

    auto bad = [data = large_data]() {
        return data.size();
    };

    auto good = [data = std::move(large_data)]() {
        return data.size();
    };

    std::string long_str(10'000, 'a');

    auto bad_str = [s = long_str]() { return s.length(); };
    auto good_str = [s = std::move(long_str)]() { return s.length(); };
}

Debugging checklist

  • After std::move into a capture, treat the outer name as empty unless you reset it.
  • Watch lifetimes for reference captures.
  • Add mutable when members must change.
  • Be explicit about copy vs move.
  • Synchronize if multiple threads call the same mutable closure.

Performance considerations

Copy vs move benchmark

#include <chrono>
#include <vector>
#include <iostream>

void performance_comparison() {
    const int SIZE = 10'000'000;
    std::vector<int> data(SIZE, 42);

    auto start = std::chrono::high_resolution_clock::now();
    auto copy_lambda = [data]() { return data.size(); };
    copy_lambda();
    auto end = std::chrono::high_resolution_clock::now();
    auto copy_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    data = std::vector<int>(SIZE, 42);
    start = std::chrono::high_resolution_clock::now();
    auto move_lambda = [data = std::move(data)]() { return data.size(); };
    move_lambda();
    end = std::chrono::high_resolution_clock::now();
    auto move_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    std::cout << "copy capture: " << copy_time.count() << " μs\n";
    std::cout << "move capture: " << move_time.count() << " μs\n";
}

Closure size (illustrative)

void memory_usage() {
    auto unique_lambda = [p = std::make_unique<int>(42)]() {};
    std::cout << "unique_ptr closure: " << sizeof(unique_lambda) << " bytes\n";

    auto shared_lambda = [p = std::make_shared<int>(42)]() {};
    std::cout << "shared_ptr closure: " << sizeof(shared_lambda) << " bytes\n";

    std::vector<int> vec(1000);
    auto value_lambda = [vec]() {};
    std::cout << "vector by-value closure: " << sizeof(value_lambda) << " bytes\n";

    auto ref_lambda = [&vec]() {};
    std::cout << "vector by-reference closure: " << sizeof(ref_lambda) << " bytes\n";
}

Best practices and guidelines

1. Prefer explicit capture

auto good1 = [x, y]() { return x + y; };
auto good2 = [ptr = std::move(ptr)]() { ptr->use(); };

2. Naming

Use suffixes or distinct names (ptr_, conn) when it clarifies move semantics.

3. Type choice

Use unique_ptr for exclusive ownership, shared_ptr when sharing or std::function requires copyability, and move capture for large vectors.

4. Error handling

Keep RAII inside the closure so resources are released on exit paths.


Language version overview

FeatureC++11C++14C++17C++20
Basic [x], [&x]YesYesYesYes
Init-capture [x = expr]NoYesYesYes
[*this]NoNoYesYes
[=, this] warnings / fixesYesYes
Template lambdasNoYesYesYes
constexpr lambdaNoNoYesYes
[...]<typename T>NoNoNoYes

Summary and quick reference

TopicC++11C++14
Default capture[x], [&x], [=], [&]Same
Init-captureNot available[name = expr]
Move captureExtra locals[name = std::move(x)]
Transformed captureExtra locals[doubled = x * 2]

Syntax cheat sheet

[x]()                      // copy x
[&x]()                     // reference x
[x = expr]()               // member initialized from expr
[x = std::move(y)]()       // move into member
[x, &y, z = std::move(w)]()  // mixed
[x = 0]() mutable { ++x; }

Decision flow (simplified)

Need a capture?
├─ Copyable small value?     → [x]
├─ Move-only / large buffer? → [x = std::move(y)]
├─ Shared across owners?     → shared_ptr + [p]
└─ Long-lived alias?         → [&x] only if lifetime is clear

Mistake summary

MistakeSymptomMitigation
Use after moveCrash, UBCheck nullptr, rename after move
Dangling referenceRandom faultsMove-by-value or shared_ptr
Missing mutableCompile errorAdd mutable
Unintended copySlowstd::move in init-capture
Thread safetyData racesatomics / mutexes

See also: Lambda capture, make_unique & make_shared, Custom deleters.



Further reading


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. C++11 capture vs C++14 init-capture ([x=expr]): move semantics, unique_ptr, async patterns, and common lifetime mistakes…

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++, lambda, init-capture, C++14, move semantics, unique_ptr.