close up photo of programming of codes

codecungnhau.com

Một trang web về kỹ thuật lập trình

5 Tò mò về sử dụng Lambda trong C++

Bạn có biết làm thế nào để viết một lambda đệ quy? Lưu trữ chúng trong một container? Hay gọi vào lúc biên dịch?. Trong bài viết này, tôi sẽ giới thiệu cho các bạn biết về các sử dụng Lamda trong các ngữ cảnh đó.

Đệ quy lambda với std::function

Viết một hàm đệ quy tương đối đơn giản: bên trong một định nghĩa hàm, bạn có thể gọi lại hàm đó bằng tên của nó. Còn với lambda thì làm như thế nào?

int main() {
    auto factorial = [](int n) {
        return n > 1 ? n * factorial(n - 1) : 1;
    };
    return factorial(5);
}

Thật không may, đoạn code trên không biên dịch được. Vì vậy, bằng cách sử dụng std::function để capture factorial và có thể sử dụng nó bên trong thân lambda

#include <functional>
int main() {
    const std::function<int(int)> factorial = [&factorial](int n) {
        return n > 1 ? n * factorial(n - 1) : 1;
    };
    return factorial(5);
}

Và kể từ C++14, chúng ta cũng có thể tận dụng lambda lồng như sau đây:

int main() {
    const auto factorial = [](int n) {
        const auto fact_impl = [](int n, const auto& impl) -> int {
            return n > 1 ? n * impl(n - 1, impl) : 1;
        };
        return fact_impl(n, fact_impl);
    };
    return factorial(5);
}

Thậm chí lần này, nó còn phức tạp hơn (nhưng cần phải sử dụng std::function). Nó sử dụng lambda lồng bên trong cho tính toán trong hàm main và sau đó, truyền nó vào như là một đối số của hàm. Nhưng tôi tự hỏi: bạn đã bao giờ sử dụng lambda đệ quy? Hoặc tốt hơn là dựa vào các hàm đệ quy (cái mà có vẻ thoải mái hơn nhiều để sử dụng và viết).

Hằng biểu thức (constexpr) lambda

Kể từ C++17, chúng ta có thể viết lambda có toán tử gọi được định nghĩa là constexpr. Chúng ta có thể sử dụng thuộc tính này và mở rộng ví dụ đệ quy thành:

int main() {
    constexpr auto factorial = [](int n) {
        constexpr auto fact_impl = [](int n, const auto& impl) -> int {
            return n > 1 ? n * impl(n - 1, impl) : 1;
        };
        return fact_impl(n, fact_impl);
    };
    static_assert(factorial(5) == 120);
}

Và trong C++20, bạn thậm chí có thể áp dụng consteval để đánh dấu lambda chỉ có thể được đánh giá tại thời điểm biên dịch.

Lưu trữ lambda trong một Container

Điều này có thể là một chút gian lận nhưng chúng ta có thể lưu trữ lambda trong một container.

Trong khi các kiểu đóng có các hàm tạo mặc định đã bị xóa (trừ khi nó là lambda phi trạng thái trong C++20), chúng ta có thể thực hiện một chút hack và lưu trữ tất cả lambda dưới dạng các đối tượng std::function. Ví dụ:

#include <functional>
#include <iostream>
#include <vector>
int main() {
    std::vector<std::function<std::string(const std::string&)>> vecFilters;
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " Amazing"; 
    });
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " Modern"; 
    });
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " C++"; 
    });
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " World!"; 
    });
    const std::string str = "Hello";
    auto temp = str;
    for (auto &entryFunc : vecFilters)  
        temp = entryFunc(temp);
    std::cout << temp;
}

Generic lambda và sự hỗ trợ nội suy kiểu

C++14 đã mang đến một sự bổ sung quan trọng cho lambda: các đối số lambda chung. Dưới đây, một ví dụ cho thấy tại sao nó hữu ích

#include <algorithm>
#include <iostream>
#include <map>
#include <string>
int main() {
    const std::map<std::string, int> numbers { 
        { "one", 1 }, {"two", 2 }, { "three", 3 }
    };
    std::for_each(std::begin(numbers), std::end(numbers), 
         [](const std::pair<std::string, int>& entry) {
             std::cout << entry.first << " = " << entry.second << '\n';
         }
    );
}

Bạn có thấy cái sai ở đây là gì không? Có phảii là đối số được chỉ định ở trong lambda bên trong for_each? Cụ thể nó nằm trong const std::pair<std::string, int>& entry. Nó sai vì cặp khóa/giá trị bên trong map là std::pair<const std::string, int>. Đó là lý do tại sao trình biên dịch phải tạo các bản sao tạm thời không mong muốn và sau đó truyền chúng đến lambda. Chúng ta có thể nhanh chóng khắc phục điều này bằng cách sử dụng generic lambda từ C++14.

std::for_each(std::begin(numbers), std::end(numbers), 
    [](const auto& entry) {
        std::cout << entry.first << " = " << entry.second << '\n';
    }
);

Bây giờ thì vừa phù hợp về kiểu vừa không có bản sao bổ sung được tạo ra.

Trả về một lambda

Nếu bạn muốn trả lại lambda từ một hàm, thì nó không đơn giản vì bạn không biết chính xác kiểu của đối tượng. Trong C++11, chúng ta có thể sử dụng hàm std::function

#include <functional>
std::function<int(int)> CreateLambda(int y) {
    return [y](int x) { return x + y; };
}
int main() {
    auto lam = CreateLambda(10);
    return lam(32);
}

Còn trong C++14, thì ta có thể sử dụng auto để suy luận ra được kiểu của đối tượng.

auto CreateLambda(int y) {
    return [y](int x) { return x + y; };
}
int main() {
    auto lam = CreateLambda(10);
    return lam(32);
}

Đã đăng vào

trong

bởi

Bình luận

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *