快盘下载:好资源、好软件、快快下载吧!

快盘排行|快盘最新

当前位置:首页软件教程电脑软件教程 → C++-lambda-学习笔记

C++-lambda-学习笔记

时间:2022-10-02 07:29:18人气:作者:快盘下载我要评论

放入lambda主体中的代码会被“转换”为对应闭包类型的()运算符中的代码。
默认情况下;在c;;11中;它是一个const内联成员函数。例如:


auto lam = [](double param) { /* do something*/ };

扩展为:


struct __anonymousLambda
{
    inline void operator()(double param) const
    { /* do something */
    }
};

当定义lambda时;无法创建接受不同参数的“重载”lambda。如:


// doesn;t compile!
auto lam = [](double param) { /* do something*/ };
auto lam = [](int param) { /* do something*/ };


test.cpp: In function ;int main();:
test.cpp:6:10: error: conflicting declaration ;auto lam;
    6 |     auto lam = [](int param) { /* do something*/ };
      |          ^~~
test.cpp:5:10: note: previous declaration as ;main()::<lambda(double)> lam;
    5 |     auto lam = [](double param) { /* do something*/ };

[]不仅引入了lambda表达式;还保存了一个捕获变量的列表。它被称为“捕获子句”。通过从lambda表达式外部捕获变量;可以在闭包类型中创建成员变量(非静态数据成员)。然后;在lambda表达式的主体中;可以访问它。
在c;; 98/03中对PrintFunctor做了类似的处理。在这个类中;添加了一个成员变量std::string strText;它是在构造函数中初始化的。成员变量允许在callable对象中存储状态

捕获的语法:
[&] 通过引用捕获在可到达作用域中声明的所有自动存储期变量。
[=] 通过值捕获(创建副本)在到达作用域中声明的所有自动存储期变量。
[x; &y]:显式地通过值捕获x;通过引用捕获y。
(args ……)通过值捕获模板参数包。
(&args ……)通过引用捕获模板参数包。


int x = 2 , y = 3 ;
const auto l1 = []() { return 1 ; }; // No capture
const auto l2 = [=]() { return x; }; // All by value (copy)
const auto l3 = [&]() { return y; }; // All by ref
const auto l4 = [x]() { return x; }; // Only x by value (copy)
// const auto lx = [=x]() { return x; }; // wrong syntax, no need for
// = to copy x explicitly
const auto l5 = [&y]() { return y; }; // Only y by ref
const auto l6 = [x, &y]() { return x * y; }; // x by value and y by ref
const auto l7 = [=, &x]() { return x ; y; }; // All by value except x
// which is by ref
const auto l8 = [&, y]() { return x - y; }; // All by ref except y which
// is by value


std::string str{;Hello World;};
auto foo = [str](){ std::cout << str << ;
;; };
foo();

对于上面的lambda表达式;str被value捕获(即它被复制)。编译器可能会生成如下的局部函数:


struct _unnamedLambda
{
    _unnamedLambda(std::string s) : str(s) {} // copy
    void operator() const
    {
        std::cout << str << ;
;;
    }
    std::string str;
};

将一个变量传递给构造函数;这在概念上称为lambda声明的“in-place”。
在上面展示的一个可能的构造函数(_unnamedLambda)仅用于演示目的;因为编译器可能以不同的方式实现它;并且不会公开它。


int x = 1, y = 1;
std::cout << x << ; ; << y << std::endl;
const auto foo = [&x, &y]() noexcept{ ;; x; ;; y; };
foo();
std::cout << x << ; ; << y << std::endl

对于上面的lambda表达式;编译器可能会生成如下的局部函子:


struct _unnamedLambda
{
    _unnamedLambda(int &a, int &b) : x(a), y(b) {}
    void operator() const noexcept
    {
        ;;x;
        ;;y;
    }
    int &x;
    int &y;
};

由于通过引用捕获x和y;因此闭包类型将包含同样是引用的成员变量。
虽然指定[=]或[&]可能很方便;因为它会捕获所有自动存储期变量;但显式地捕获变量更清晰。这样编译器就可以警告不必要的影响(参见关于全局和静态变量的说明)

The mutable Keyword
默认情况下;闭包类型的()运算符被标记为const;不能在lambda表达式的内部修改捕获的变量。如果想改变这种行为;需要在参数列表后面添加mutable关键字。这种语法实际上从闭包类型的调用操作符声明中删除了const。如果有一个简单的lambda表达式和一个可变的:


int x = 1 ;
auto foo = [x]() mutable { ;; x; };

它将被“扩展”为以下functor:


struct __lambda_x1
{
    void operator()() { ;;x; }
    int x;
};

可以看到;调用操作符可以更改成员字段的值。



#include <iostream>
int main()
{
    const auto print = [](const char *str, int x, int y)
    {
        std::cout << str << ;: ; << x << ; ; << y << ;
;;
    };
    int x = 1, y = 1;
    print(;in main();, x, y);
    auto foo = [x, y, &print]() mutable
    {
        ;;x;
        ;;y;
        print(;in foo();, x, y);
    };
    foo();
    print(;in main();, x, y);
}


in main(): 1 1
in foo(): 2 2
in main(): 1 1

在上面的例子中;可以改变x和y的值。因为它们只是所在作用域中x和y的副本;所以在foo被调用后;不会看到它们的新值。另一方面;如果是通过引用捕获的;那么就不需要对lambda应用mutable来修改值。这是因为捕获的成员变量是引用;不能在const成员函数中绑定;但可以更改引用的值。


int x = 1;
std::cout << x << ;
;;
const auto foo = [&x]() noexcept{ ;;x; };
foo();
std::cout << x << ;
;;

在上面的例子中;可以改变x和y的值。因为它们只是所在作用域中x和y的副本;所以在foo被调用后;不会看到它们的新值。另一方面;如果是通过引用捕获的;那么就不需要对lambda应用mutable来修改值。这是因为捕获的成员变量是引用;不能在const成员函数中绑定;但可以更改引用的值。


int x = 10 ;
const auto lam = [x]() mutable { ;; x; }
lam(); // doesn;t compile!

最后一行无法编译;因为无法在const对象上调用非const成员函数。
在继续讨论一些更复杂的捕获主题之前;可以稍微休息一下;专注于一个更实际的例子。当想使用标准库中的一些现有算法并改变默认行为时;Lambda表达式很方便。例如;对于std::sort;可以编写自己的比较函数。但是可以更进一步;使用调用计数器来增强comparator。看一看:


#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
    std::vector<int> vec{0, 5, 2, 9, 7, 6, 1, 3, 4, 8};
    size_t compCounter = 0;
    std::sort(vec.begin(), vec.end(),
              [&compCounter](int a, int b) noexcept
              {
                  ;;compCounter;
                  return a < b;
              });
    std::cout << ;number of comparisons: ; << compCounter << ;
;;
    for (const auto &v : vec)
        std::cout << v << ;, ;;
    return 0;
}


示例中提供的comparator与默认的comparator的工作方式相同;如果a小于b;它就返回;因此使用从最小到最大的自然顺序。不过;传递给std::sort的lambda表达式也会捕获局部变量compCounter。然后用这个变量来计算排序算法对这个比较器的调用次数。


number of comparisons: 36
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

如果有一个全局变量;并在lambda表达式中使用[=];可能会认为的全局对象也可以被value捕获……但事实并非如此。看代码:


#include <iostream>
int global = 10;
int main()
{
    std::cout << global << std::endl;
    auto foo = [=]() mutable noexcept{ ;;global; };
    foo();
    std::cout << global << std::endl;
    const auto increaseGlobal = []() noexcept { ;;global; };
    increaseGlobal();
    std::cout << global << std::endl;
    const auto moreIncreaseGlobal = [global]() noexcept { ;;global; };
    moreIncreaseGlobal();
    std::cout << global << std::endl;
    
    return 0;
}

上面的例子定义了global;然后将它与main()函数中定义的几个lambda一起使用。如果运行代码;那么无论以何种方式捕获;它都将始终指向全局对象;并且不会创建本地副本。这是因为只有具有自动存储期的变量才能被捕获。GCC甚至会报告以下警告:


 ;capture of variable ;global; with non-automatic storage duration;

只有当显式地捕获全局变量时;这个警告才会出现;所以如果使用[=];编译器不会帮助。Clang编译器甚至更有用;因为它会生成一个错误:


error: ;global; cannot be captured because it does not have
automatic storage duration

与捕获全局变量类似;静态对象也会遇到同样的问题:


#include <iostream>
void bar()
{
    static int static_int = 10;
    std::cout << static_int << std::endl;
    auto foo = [=]() mutable noexcept { ;;static_int; };
    foo();
    std::cout << static_int << std::endl;
    const auto increase = []() noexcept { ;;static_int; };
    increase();
    std::cout << static_int << std::endl;
    const auto moreIncrease = [static_int] () noexcept { ;;static_int; };
    moreIncrease();
    std::cout << static_int << std::endl;
}
int main()
{
    bar();
}

这一次;试图捕获一个静态变量;然后改变它的值;但由于它没有自动存储期;编译器无法做到这一点。输出:


10
11
12
13

当捕获名为[static_int]的变量时;GCC会报告一个警告;而Clang会显示一个错误
在类成员函数中;如果想捕获成员变量;事情会变得更加复杂。由于所有数据成员都与this指针相关;因此它也必须存储在某个地方。


#include <iostream>
struct Baz
{
    void foo()
    {
        const auto lam = [s](){ std::cout << s; };
        lam();
    }
    std::string s;
};
int main()
{
    Baz b;
    b.foo();
}

代码试图捕获成员变量s。但是编译器会报错:


test.cpp: In member function ;void Baz::foo();:
test.cpp:6:27: error: capture of non-variable ;Baz::s;
    6 |         const auto lam = [s](){ std::cout << s; };
      |                           ^
test.cpp:9:17: note: ;std::string Baz::s; declared here
    9 |     std::string s;
      |                 ^
test.cpp: In lambda function:
test.cpp:6:46: error: ;this; was not captured for this lambda function
    6 |         const auto lam = [s](){ std::cout << s; };
      |                                              ^
test.cpp:6:46: error: invalid use of non-static data member ;Baz::s;
test.cpp:9:17: note: declared here
    9 |     std::string s;

要解决这个问题;必须捕获this指针。然后就可以访问成员变量。


#include <iostream>
struct Baz
{
    void foo()
    {
        const auto lam = [this](){ std::cout << s <<std::endl; };
        lam();
    }
    std::string s{;test;};
};
int main()
{
    Baz b;
    b.foo();
}

现在没有编译错误了。也可以使用[=]或[&]来捕获它(它们在c;; 11/14中具有相同的效果!)。请注意;通过值捕获到一个指针。这就是为什么可以访问成员变量;而不是它的副本。在c;;11中(甚至在c;; 14中);都不能这样写:


auto lam = [* this ]() { std:: cout << s; };;

代码无法在c;; 11/14中编译;但是;在c;;17中是允许的
如果在一个方法的上下文中使用lambda表达式;那么捕获它就很好。但是更复杂的情况呢?知道下面的代码会发生什么吗?


#include <functional>
#include <iostream>
struct Baz
{
    std::function<void()> foo()
    {
        return [=]{ std::cout << s << std::endl; };
    }
    std::string s;
};
int main()
{
    auto f1 = Baz{;abc;}.foo();
    auto f2 = Baz{;xyz;}.foo();
    f1();
    f2();
}

这段代码声明了一个Baz对象;然后调用foo()。请注意;foo()返回的lambda(存储在std::function中)捕获了类的成员。4由于使用临时对象;不能确定当调用f1和f2时会发生什么。这是一个悬空引用问题;会产生未定义的行为。


struct Bar
{
    std::string const &foo() const { return s; };
    std::string s;
};
auto &&f1 = Bar{;abc;}.foo(); // a dangling reference

同样;如果显式地声明捕获([s]):


std::function<void()> foo()
{
    return [s]{ std::cout << s << std::endl; };
}

总而言之;当lambda的寿命比对象本身长时;捕获这种情况可能会变得棘手。当使用异步调用或多线程时;可能会发生这种情况

如果有一个只能移动的对象(例如unique_ptr);那么就不能将它作为捕获变量移动到lambda。通过值进行捕获是行不通的;您只能通过引用捕获。


std::unique_ptr<int> p(new int{10});
auto foo = [p]() {};      // does not compile....
auto foo_ref = [&p]() {}; // compiles, but the ownership
                          // is not passed

在上面的例子中;可以看到捕获unique_ptr的唯一方法是通过引用。然而;这种方法可能不是最好的;因为它不转移指针的所有权。关于c;;14中;会看到这个问题被修复了;这要归功于捕获与初始化器。
如果捕获了一个常量变量;那么这个常量就会被保留:


#include <iostream>
#include <type_traits>
int main()
{
    const int x = 10;
    auto foo = [x]() mutable
    {
        std::cout << std::is_const<decltype(x)>::value << std::endl;
        x = 11;
    };
    foo();
}

上面的代码无法编译;因为捕获的变量是常量。这个例子甚至尝试使用mutable;但这无济于事。
为了结束对capture子句的讨论;应该提到;还可以利用可变参数模板来捕获数据。编译器将包扩展为一个非静态数据成员的列表;如果想在模板代码中使用lambda;这可能很方便。例如;下面是一个实验捕获的代码示例:


#include <iostream>
#include <tuple>
template <class... Args>
void captureTest(Args... args)
{
    const auto lambda = [args...]
    {
        const auto tup = std::make_tuple(args...);
        std::cout << ;tuple size: ; << std::tuple_size<decltype(tup)>::value << ;
;;
        std::cout << ;tuple 1st: ; << std::get<0>(tup) << ;
;;
    };
    lambda(); // call it
}
int main()
{
    captureTest(1, 2, 3, 4);
    captureTest(;Hello world;, 10.0f);
}


tuple size: 4
tuple 1st: 1
tuple size: 2
tuple 1st: Hello world

这段有点实验性的代码表明;可以通过值(也可以通过引用)捕获可变参数包;然后将包“存储”到元组对象中。然后;调用元组上的一些辅助函数来访问它的数据和属性。还可以使用c;;洞察来查看编译器如何生成代码并将模板、参数包和lambda表达式扩展为代码。请看这里的例子c;; Insight。
在很多情况下;可以跳过lambda的返回类型;然后编译器会替推断出typename。最初;返回类型推断仅限于主体中包含单个return语句的lambda表达式。然而;这个限制很快就被取消了;因为实现一个更方便的版本没有问题。从c;; 11开始;只要所有的return语句都是相同类型的;编译器就能够推断出返回类型。


#include <type_traits>
int main()
{
    const auto baz = [](int x) noexcept
    {
        if (x < 20)
            return x * 1.1;
        else
            return x * 2.1;
    };
    static_assert(std::is_same<double, decltype(baz(10))>::value,
                  ;has to be the same!;);
}

在上面的lambda中;有两个return语句;但它们都指向double;因此编译器可以推断出类型。
如果想显式地指定返回类型;可以使用后面的返回类型规范。例如;当返回一个字符串字面量时:


#include <iostream>
#include <string>
int main()
{
    const auto tesTSPeedString = [](int speed) noexcept
    {
        if (speed > 100)
            return ;you;re a super fast;;
        return ;you;re a regular;;
    };
    auto str = testSpeedString(100);
    str ;= ; driver;; // uups! no ;= on const char*!
    std::cout << str;
    return 0;
}

上面的代码无法编译;因为编译器推断出lambda的返回类型是const char*。这是因为字符串字面量上没有;=运算符;因此代码中断。

可以通过显式地将返回类型设置为std::string来解决这个问题:


auto testSpeedString = [](int speed) -> std::string
{
    if (speed > 100)
        return ;you;re a super fast;;
    return ;you;re a regular;;
};
auto str = testSpeedString(100);
str ;= ; driver;; // works fine

请注意;现在必须删除noexcept;因为创建std::string时可能会抛出异常。顺便提一下;还可以使用命名空间std::string_literals;然后返回“you’re a regular”表示std::string类型
为了说明lambda如何支持这种转换;让考虑下面的例子。它定义了一个显式定义转换操作符的函子baz:


#include <iostream>
void callWith10(void (*bar)(int))
{
    bar(10);
}
int main()
{
    struct
    {
        using f_ptr = void (*)(int);
        void operator()(int s) const { return call(s); }
        operator f_ptr() const { return &call; }
    private:
        static void call(int s) { std::cout << s << ;
;; };
    } baz;
    callWith10(baz);
    callWith10([](int x){ std::cout << x << ;
;; });
}

在上述程序中;有一个函数callWith10接受一个函数指针作为参数。然后使用两个参数调用它(第18行和第19行):第一个使用baz;这是一个包含必要的转换操作符的functor -它转换为f_ptr;这与callWith10的输入参数相同。稍后;会调用lambda表达式。在这种情况下;编译器在底层执行所需的转换。
当需要调用需要回调的c风格函数时;这种转换可能很方便。例如;下面的代码调用了C库中的qsort;并使用lambda对元素进行逆序排序:


#include <cstdlib>
#include <iostream>
int main()
{
    int values[] = {8, 9, 2, 5, 1, 4, 7, 3, 6};
    constexpr size_t numElements = sizeof(values) / sizeof(values[0]);
    std::qsort(values, numElements, sizeof(int),
               [](const void *a, const void *b) noexcept
               {
                   return (*(int *)b - *(int *)a);
               });
    for (const auto &val : values)
        std::cout << val << ;, ;;
}

正如在代码示例中看到的那样;std::qsort只使用函数指针作为比较器。编译器可以对传入的无状态lambda表达式进行隐式转换。
在继续讨论另一个话题之前;还有一个案例值得分析:


#include <type_traits>
int main()
{
    auto funcPtr = ;[] {};
    static_assert(std::is_same<decltype(funcPtr), void (*)()>::value);
}

请注意;的奇怪语法。如果移除加号;那么static_assert会失败。这是为什么呢?
代码中使用了一元运算符;。这个运算符可以作用于指针;因此编译器会将无状态lambda转换为函数指针;然后将其赋值给funcPtr。另一方面;如果删除加号;那么funcPtr就只是一个普通的闭包对象;这就是static_assert失败的原因。虽然用“;”编写这样的语法可能不是最好的主意;但如果编写static_cast;它具有相同的效果。当不希望编译器创建太多函数实例时;可以使用这种技术。例如:


template <typename F>
void call_function(F f)
{
    f(10);
}
int main()
{
    call_function(static_cast<int (*)(int)>([](int x){ return x ; 2; }));
    call_function(static_cast<int (*)(int)>([](int x){ return x * 2; }));
}

在上面的例子中;编译器只需创建call_function的单个实例——因为它只接受一个函数指针int (*)(int)。但是如果移除static_cast;那么会得到两个版本的call_function;因为编译器必须为lambda创建两个单独的类型
在大多数例子中;可以注意到我定义了一个lambda表达式;然后调用它。
不过;也可以立即调用lambda:


#include <iostream>
int main()
{
    int x = 1, y = 1;
    [&]() noexcept{ ;; x; ;; y; }(); // <-- call ()
    std::cout << x << ;, ; << y;
}

正如在上面看到的;lambda被创建并且没有被赋值给任何闭包对象。但随后它被调用与()。如果运行这个程序;会看到输出为2,2。
当需要对const对象进行复杂的初始化时;这种表达式可能很有用。


const auto val = []()
{
    /* several lines of code... */
}(); // call it!

上面的val是lambda表达式返回类型的常量;即:


// val1 is int
const auto val1 = []() { return 10 ; }();
// val2 is std::string
const auto val2 = []() -> std:: string { return ;ABC; ; }();


#include <iostream>
#include <string>
void ValidateHTML(const std::string &) {}
std::string BuildAHref(const std::string &link, const std::string &text)
{
    const std::string html = [&link, &text]
    {
        const auto &inText = text.empty() ? link : text;
        return ;<a href= ; ; ; link ; ; ; >; ; inText ; ;</a>;;
    }(); // call!
    ValidateHTML(html);
    return html;
}
int main()
{
    try
    {
        const auto ahref = BuildAHref(;www.leanpub.com;, ;Leanpub Store;);
        std::cout << ahref;
    }
    catch (...)
    {
        std::cout << ;bad format...;;
    }
}

上面的例子包含一个函数BuildAHref;它接受两个参数;然后构建一个 HTML标签。根据输入参数;构建html变量。如果文本不为空;则使用它作为内部HTML值。否则;使用链接。希望html变量是const;但在输入参数中添加必要的条件;很难编写紧凑的代码。多亏了IIFE;可以写一个单独的lambda表达式;然后用const标记变量。稍后可以将该变量传递给ValidateHTML。

可能会惊讶地发现;也可以从lambda派生!因为编译器使用operator()将lambda表达式扩展为一个functor对象;所以可以继承这个类型。


#include <iostream>
template <typename Callable>
class ComplexFunctor : public Callable
{
public:
    explicit ComplexFunctor(Callable f) : Callable(f) {}
};
template <typename Callable>
ComplexFunctor<Callable> MakeComplexFunctor(Callable &&cal)
{
    return ComplexFunctor<Callable>(cal);
}
int main()
{
    const auto func = MakeComplexFunctor([](){ std::cout << ;Hello Functor!;; });
    func();
}

在这个例子中;ComplexFunctor类派生自一个模板参数Callable。如果想从lambda表达式中派生;需要做一个小技巧;因为无法明确说出闭包类型的确切类型(除非把它包装在std::function中)。这就是为什么需要MakeComplexFunctor函数;它可以执行模板参数推断;并得到lambda闭包的类型。除了名字之外;ComplexFunctor只是一个简单的包装器;没有太多用途。这种代码模式有什么用例吗?例如;可以扩展上面的代码并继承两个lambda并创建一个重载的集合:

这一次有更多的代码:从两个模板参数派生;但还需要显式地暴露它们的调用操作符。这是为什么呢?这是因为在寻
找正确的函数重载时;编译器要求候选函数在相同的作用域内。为了理解这一点;让写一个派生于两个基类的简单类型。这个例子还注释了两个using语句:


#include <iostream>
struct BaseInt
{
    void Func(int) { std::cout << ;BaseInt... 
 ;; }
};
struct BaseDouble
{
    void Func(double) { std::cout << ;BaseDouble... 
 ;; }
};
struct Derived : public BaseInt, BaseDouble
{
    // using BaseInt::Func;
    // using BaseDouble::Func;
};
int main()
{
    Derived d;
    d.Func(10.0);
}

将lambda表达式存储在容器中


#include <iostream>
#include <vector>
int main()
{
    using TFunc = void (*)(int &);
    std::vector<TFunc> ptrFuncVec;
    ptrFuncVec.push_back([](int &x){ std::cout << x << ;
;; });
    ptrFuncVec.push_back([](int &x){ x *= 2; });
    ptrFuncVec.push_back(ptrFuncVec[0]); // print it again;
    int x = 10;
    for (const auto &entry : ptrFuncVec)
        entry(x);
    return 0;
}


#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
int main()
{
    std::vector<std::function<std::string(const std::string &)>> vecFilters;
    size_t removedSpaceCounter = 0;
    const auto removeSpaces = [&removedSpaceCounter](const std::string &str)
    {
        std::string tmp;
        std::copy_if(str.begin(), str.end(), std::back_inserter(tmp),
                    [](char ch){ return !isspace(ch); });
        removedSpaceCounter ;= str.length() - tmp.length();
        return tmp;
    };
    const auto makeUpperCase = [](const std::string &str)
    {
        std::string tmp = str;
        std::transform(tmp.begin(), tmp.end(), tmp.begin(),
                       [](unsigned char c){ return std::toupper(c); });
        return tmp;
    };
    vecFilters.emplace_back(removeSpaces);
    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!;; });
    vecFilters.emplace_back(makeUpperCase);
    const std::string str = ; H e l l o ;;
    auto temp = str;
    for (const auto &entryFunc : vecFilters)
        temp = entryFunc(temp);
    std::cout << temp;
    std::cout << ; 
 removed spaces: ; << removedSpaceCounter << ;
;;
}

这次在容器中存储了std::function<std::string(const std::string&)>。这允许使用任何类型的函数对象;包括带捕获变量的lambda表达式。其中一个lambda removeSpacesCnt捕获了一个变量;该变量用于存储输入字符串中被删除的空格的信息。

C;;14
在c;; 14中;可以在函数调用中使用默认参数。这是一个小特性;但使lambda更像一个常规函数


#include <iostream>
int main()
{
    const auto lam = [](int x = 10){ std::cout << x << ;
;; };
    lam();
    lam(100);
}

如果有多个return语句;它们都必须推断出相同的类型:


auto foo = [](int x)
{
    if (x < 0)
        return x * 1.1f; // float!
    else
        return x * 2.1; // double!
};

上面的代码无法编译;因为第一个return语句返回float而第二个double。编译器无法决定;所以必须选择单一类型。虽然推断整数或双精度浮点数可能很有用;但返回类型推断的价值还有更重要的原因。此功能在模板代码和“未知”事物中发挥了相当大的作用。例如;lambda闭包类型是匿名的;不能在代码中显式指定它。如果想从函数返回一个lambda;那么如何指定类型?


#include <functional>
#include <iostream>
std::function<int(int)> CreateMulLambda(int x)
{
    return [x](int param) noexcept{ return x * param; };
}
int main()
{
    const auto lam = CreateMulLambda(10);
    std::cout << sizeof(lam);
    return lam(2);
}

然而;上述解决方案并不简单。它需要指定函数签名;甚至包括一些额外的头文件。回想一下在c;; 11章中;std::function是一个重量级对象(在GCC 9中;sizeof显示32字节);它需要一些高级的内部机制才能处理任何可调用对象。多亏了c;; 14的改进;现在可以大大简化代码:


#include <iostream>
auto CreateMulLambda(int x) noexcept
{
    return [x](int param) noexcept{ return x * param; };
}
int main()
{
    const auto lam = CreateMulLambda(10);
    std::cout << sizeof(lam);
    return lam(2);
}

这一次可以完全依赖编译时类型推断;不需要任何辅助类型。在GCC上;lambda sizeof(lam)的大小只有4字节;而且比使用std::function的解决方案要便宜得多。请注意;还可以将CreateMulLambda标记为noexcept;因为它不会抛出任何异常。在返回std::function时;情况并非如此。

现在有一些更重要的更新!应该还记得;在lambda表达式中;可以从外部作用域捕获变量。编译器扩展捕获语法并在闭包类型中创建成员变量(非静态数据成员)。现在;在c;;14中;可以创建新的成员变量并在capture子句中初始化它们。然后可以在lambda中访问这些变量。它被称为带有初始化器的捕获;或者这个功能的另一个名字是泛化的lambda捕获


#include <iostream>
int main()
{
    int x = 30;
    int y = 12;
    const auto foo = [z = x ; y](){ std::cout << z << ;
;; };
    x = 0;
    y = 0;
    foo();
}


42

在上面的例子中;编译器生成了一个新的成员变量;并用x;y初始化它。推断新变量的类型的方式与将auto放在这个变量前面的方式相同。在的例子中:


auto z = x ; y;

总而言之;上例中的lambda表达式会被解析为如下(简化后的)functor:


struct _unnamedLambda
{
    void operator()() const
    {
        std::cout << z << ;
;;
    }
    int z;
} someInstance;

在定义lambda表达式时;Z会直接初始化(用x;y)。记住前面的句子。新变量是在定义lambda表达式的地方初始化的;而不是在调用它的地方。这就是为什么如果在创建lambda之后修改x或y变量;变量z不会改变。在这个例子中;可以看到;在定义lambda表达式之后;我立即改变了x和y的值。然而;由于z之前被初始化;输出仍然是42。
通过初始化器创建变量也很灵活;例如;可以从外部作用域创建对变量的引用。


#include <iostream>
int main()
{
    int x = 30;
    const auto foo = [&z = x](){ std::cout << z << ;
;; };
    foo();
    x = 0;
    foo();
}

这一次;变量z是对x的引用。它的创建方式和写的一样:


auto & z = x;

如果运行这个例子;应该看到第一行打印了30;而第二行显示了0。这是因为捕获了一个引用;所以当修改引用的变量时;z对象也会改变。

请注意;虽然可以用initialiser作为引用来捕获;但不能写r值引用&&。这就是下面的代码无效的原因:


[&& z = x] // invalid syntax!

换句话说;在c;; 14中;不能这样写:


template <class... Args>
auto captureTest(Args... args)
{
    return lambda = [... capturedArgs = std::move(args)]() {};


以前;在c;; 11中;无法通过值捕获唯一的指针。只能通过引用捕获。现在;从c;; 14开始;可以将对象移动到闭包类型的成员中


#include <iostream>
#include <memory>
int main()
{
    std::unique_ptr<int> p(new int{10});
    const auto bar = [ptr = std::move(p)]
    {
        std::cout << ;pointer in lambda: ; << ptr.get() << ;
;;
    };
    std::cout << ;pointer in main(): ; << p.get() << ;
;;
    bar();
}


pointer in main(): 0
pointer in lambda: 0x1413c20

多亏了捕获初始化方法;可以将指针的所有权转移到lambda中。正如在例子中看到的;在创建闭包对象之后;唯一的指针被设置为nullptr。但当调用lambda时;会看到一个有效的内存地址


std:: unique_ptr< int > p(new int {10 });
std:: function< void ()> fn = [ptr = std:: move(p)]() { }; // won;t compile!

另一个想法是使用捕获初始化作为一种潜在的优化技术。与其每次调用lambda都计算某个值;不如在初始化器中计算一次:


#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main()
{
    using namespace std::string_literals;
    const std::vector<std::string> vs = {;apple;, ;orange;,
                                         ;foobar;, ;lemon;};
    const auto prefix = ;foo; s;
    auto result = std::find_if(vs.begin(), vs.end(),
                               [&prefix](const std::string &s)
                               {
                                   return s == prefix ; ;bar; s;
                               });
    if (result != vs.end())
        std::cout << prefix << ;-something found! 
 ;;
    result = std::find_if(vs.begin(), vs.end(),
                          [savedString = prefix ; ;bar; s](const std::string &s)
                          {
                              return s == savedString;
                          });
    if (result != vs.end())
        std::cout << prefix << ;-something found! 
 ;;
}

上面的代码显示了对std::find_if的两次调用。在第一个场景中;捕获前缀并将输入值与前缀;“bar”进行比较。每次调用lambda时;都必须创建并计算一个临时值;其中存储了这些字符串的和。对find_if的第二次调用显示了一个优化:创建了一个捕获的变量savedString;用于计算字符串的总和。然后;就可以安全地在lambda主体中引用它了。字符串的和只会运行一次;而不是每次调用lambda时都运行。这个例子还使用了std::string_literals;这就是为什么可以编写表示std::string对象的“foo”

initializer也可以用来捕获成员变量。然后;可以捕获成员变量的副本;而不用担心悬空引用。


#include <algorithm>
#include <iostream>
struct Baz
{
    auto foo() const
    {
        return [s = s]
        { std::cout << s << std::endl; };
    }
    std::string s;
};
int main()
{
    const auto f1 = Baz{;abc;}.foo();
    const auto f2 = Baz{;xyz;}.foo();
    f1();
    f2();
}

在foo()中;通过将成员变量复制到闭包类型中来捕获成员变量。此外;还使用auto来推断成员函数foo()的返回类型。顺便说一句;在c;; 11中;将不得不使用std::function;请参见c;; 11章节。在声明lambda表达式时使用了[s = s]这样的“奇怪”语法;这可能令人惊讶。这段代码之所以能工作;是因为捕获的变量在闭包类型的作用域内;而不是在外部作用域内。这就是为什么这里没有冲突

这可是个大问题!lambda的早期规范允许创建匿名函数对象;并将它们传递给标准库中的各种泛型算法。然而;闭包本身并不是“通用的”。例如;不能将模板参数指定为lambda参数。幸运的是;从c;; 14开始;标准引入了泛型lambda;现在可以这样写:


const auto foo = [](auto x, int y) { std:: cout << x << ;, ; << y << ; 
; ; };
foo(10 , 1 );
foo(10.1234 , 2 );
foo(;hello world; , 3 );

请注意;autox是lambda表达式的参数。这相当于在闭包类型的call操作符中使用模板声明:


struct
{
    template <typename T>
    void operator()(T x, int y) const
    {
        std::cout << x << ;, ;<<y << ; 
; ;
    }
} someInstance;

如果有更多的自动参数;代码将展开为单独的模板参数:


const auto fooDouble = [](auto x, auto y) { /*...*/ };

扩展为:


struct
{
    template <typename T, typename U>
    void operator()(T x, U y) const
    { /*...*/
    }
} someOtherInstance;

但这还不是全部。如果需要更多的函数参数;那么也可以使用“可变参数”


#include <iostream>
template <typename T>
auto sum(T x) { return x; }
template <typename T1, typename... T>
auto sum(T1 s, T... ts) { return s ; sum(ts...); }
int main()
{
    const auto sumLambda = [](auto... args)
    {
        std::cout << ;sum of: ; << sizeof...(args) << ; numbers 
 ;;
        return sum(args...);
    };
    std::cout << sumLambda(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
}

在上面的示例中;通用lambda使用了auto…表示可变参数包。从概念上讲;它可以扩展为以下调用操作符:


struct __anonymousLambda
{
    template <typename... T>
    void operator()(T... args) const
    { /*...*/
    }
};

使用通用lambda表达式时;不仅可以使用auto x;还可以像使用其他自动变量一样添加任何限定符;如auto&、const auto&或auto&&。其中一个方便的用例是;可以指定auto&& x;它成为一个转发(通用)引用。这允许完美地转发输入参数:


#include <iostream>
#include <string>
void foo(const std::string &) { std::cout << ;foo(const string&) 
 ;; }
void foo(std::string &&) { std::cout << ;foo(string&&) 
 ;; }
int main()
{
    const auto callFoo = [](auto &&str)
    {
        std::cout << ;Calling foo() on: ; << str << ;
;;
        foo(std::forward<decltype(str)>(str));
    };
    const std::string str = ;Hello World;;
    callFoo(str);
    callFoo(;Hello World Ref Ref;);
}


Calling foo() on: Hello World
foo(const string&)
Calling foo() on: Hello World Ref Ref
foo(string&&)

示例代码定义了两个函数重载foo;其中一个重载了对std::string的const引用;另一个重载了对std::string的r值引用。callFoo lambda使用了一个泛型参数;这是一个通用引用。如果想把这个lambda重写为一个普通的函数模板;它看起来像这样:


template <typename T>
void callFooFunc(T &&str)
{
    std::cout << ;Calling foo() on: ; << str << ;
;;
    foo(std::forward<T>(str));
}

正如在泛型lambda中看到的;有更多的选择来编写局部匿名函数。
此外;当类型推断比较棘手时;泛型lambda可能非常有用。


#include <algorithm>
#include <iostream>
#include <map>
#include <string>
int main()
{
    const std::map<std::string, int> numbers{{;one;, 1}, {;two;, 2}, {;three;, 3}};
    // each time entry is copied from pair<const string, int>!
    std::for_each(std::begin(numbers), std::end(numbers),
                  [](const std::pair<std::string, int> &entry)
                  {
                      std::cout << entry.first << ; = ; << entry.second << ;
;;
                  });
}

这是错误的;因为std::map的值类型是std::pair<const Key, T>;而不是const std::pair<Key, T>。在的例子中;由于在std::pair<const std::string, int>和const std::pair<std::string, int>&之间进行了转换;所以代码执行了额外的复制。将const std::string转换为std::string:
这个问题可以用auto来修复:


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

现在模板参数演绎将充分获得条目对象的正确类型;并且不会创建额外的副本。更不用说代码更容易阅读和更短。请参阅完整的示例;其中还包含打印各数据项地址的代码:

Lambdas in C;;17
类型系统中的异常规范
在介绍lambda的语法改进之前;需要介绍c;; 17中引入的一个“通用”语言特性。函数的异常规范过去并不属于函数类型;但现在;在c;;17中;它是函数类型的一部分。这意味着可以有两个函数重载:一个有noexcept;另一个没有。见下文:


using TNoexceptVoidFunc = void (*)() noexcept;
void SimpleNoexceptCall(TNoexceptVoidFunc f) { f(); }
using TVoidFunc = void (*)();
void SimpleCall(TVoidFunc f) { f(); }
void fNoexcept() noexcept {}
void fRegular() {}
int main()
{
    SimpleNoexceptCall(fNoexcept);
    SimpleNoexceptCall([]() noexcept {});
    // SimpleNoexceptCall(fRegular); // cannot convert
    // SimpleNoexceptCall([]() { }); // cannot convert
    SimpleCall(fNoexcept); // converts to regular function
    SimpleCall(fRegular);
    SimpleCall([]() noexcept {}); // converts
    SimpleCall([]() {});
}

指向noexcept函数的指针可以转换为指向普通函数的指针(这也适用于指向成员函数的指针和lambda表达式)。但反过来是不可能的(从普通函数指针到标记为noexcept的函数指针)。添加该功能的原因之一是有机会更好地优化代码。如果编译器能保证函数不会抛出异常;那么它可能会生成更快的代码10。在标准库中;有很多地方会检查noexcept;从而提高代码的效率。这就是std::vector的工作原理;它可以区分存储类型是否可以在移动时不抛出异常。下面是一个使用type traits和if constexpr来检查给定的可调用对象是否标记为noexcept的例子:


#include <iostream>
#include <type_traits>
template <typename Callable>
void CallWith10(Callable &&fn)
{
    if constexpr (std::is_nothrow_invocable_v<Callable, int>)
    {
        std::cout << ;Calling fn(10) with optimisation 
 ;;
        fn(10);
    }
    else
    {
        std::cout << ;Calling fn(10) normally 
 ;;
        fn(10);
    }
}
int main()
{
    int x{10};
    const auto lam = [&x](int y) noexcept { x ;= y; };
    CallWith10(lam);
    const auto lamEx = [&x](int y)
    {
        std::cout << ;lamEx with x = ; << x << ;
;;
        x ;= y;
    };
    CallWith10(lamEx);
}


Calling fn(10) with optimisation
Calling fn(10) normally
lamEx with x = 20


#include <array>
template <typename Range, typename Func, typename T>
constexpr T SimpleAccumulate(Range &&range, Func func, T init)
{
    for (auto &&elem : range)
    {
        init ;= func(elem);
    }
    return init;
}
int main()
{
    constexpr std::array arr{1, 2, 3};
    constexpr auto sum = SimpleAccumulate(arr, [](auto i){ return i * i; }, 0);
    static_assert(sum == 14);
}

这段代码使用了一个constexpr lambda表达式;它被传递给了SimpleAccumulate。lambda表达式没有显式地标记为constexpr;但编译器会用constexpr声明它的调用操作符;因为函数体中只包含一个简单的计算。该算法还使用了一些c;;17元素:constexpr对std::array、std::begin和std::end的添加现在也是constexpr;这意味着整个代码可能会在编译时执行。

在c;;14;讨论了可用于泛型lambda表达式的可变参数列表。多亏了c;; 17中的折叠表达式;可以编写更紧凑的代码。下面是转换后的求和计算示例:


#include <iostream>
int main()
{
    const auto sumLambda = [](auto... args)
    {
        std::cout << ;sum of: ; << sizeof...(args) << ; numbers 
 ;;
        return (args ; ... ; 0);
    };
    std::cout << sumLambda(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
}

如果将它与前面c;;14中的示例进行比较;会很快注意到它不需要递归!折叠表达式为提供了一种简单且相对直观的语法;用于编写组合可变参数的表达式


#include <iostream>
int main()
{
    const auto printer = [](auto... args)
    {
        (std::cout << ... << args) << ;
;;
    };
    printer(1, 2, 3, ;hello;, 10.5f);
}


123hello10.5

为了解决这个问题;可以引入一个辅助方法;将逗号运算符而不是<<折叠起来:


#include <iostream>
int main()
{
    const auto printer = [](auto... args)
    {
        const auto printElem = [](auto elem)
        {
            std::cout << elem << ;, ;;
        };
        (printElem(args), ...);
        std::cout << ;
;;
    };
    printer(1, 2, 3, ;hello;, 10.5f);
}


1, 2, 3, hello, 10.5,


const auto printer = [](auto... args)
{
    ((std::cout << args << ;, ;), ...);
    std::cout << ;
;;
};

如果不想显示打印序列末尾的最后一个逗号;可以这样做:


#include <iostream>
int main()
{
    const auto printer = [](auto first, auto... args)
    {
        std::cout << first;
        ((std::cout << ;, ; << args), ...);
        std::cout << ;
;;
    };
    printer(1, 2, 3, ;hello;, 10.5f);
}

这一次;需要为第一个条目使用一个通用模板参数;然后为其余条目使用一个可变参数列表。然后;可以打印第一个元素;并在其他元素之前添加一个逗号。代码将打印:


1, 2, 3, hello, 10.5

在c;;11章中;学习了如何从lambda表达式派生。虽然看到这样的技术很有趣;但用例是有限的。这种方法的主要问题是它只支持特定数量的lambda。这些例子使用了一两个基类。但是;如果使用数量不定的基类;也就是使用数量不定的lambda呢?在c;;17中;有一个相对简单的模式!


template < class... Ts> struct overloaded : Ts... { using Ts:: operator ()...; };
template < class... Ts> overloaded(Ts...) -> overloaded< Ts...> ;

如所见;需要使用可变参数模板;因为它们允许指定基类的可变数量。下面是一个使用该代码的简单示例:


#include <iostream>
template <class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

int main()
{
    const auto test = overloaded{ 
        [](const int &i) { std::cout << ;int: ; << i << ;
;; },
        [](const float &f) { std::cout << ;float: ; << f << ;
;; },
        [](const std::string &s) { std::cout << ;string: ; << s << ;
;; }
    };
    test(;10.0f;);
}


string: 10.0f

在上面的例子中;创建了一个由三个lambda组成的测试对象。然后可以用一个参数调用这个对象;根据输入参数的类型选择正确的lambda。现在让仔细看看这个模式的核心部分。这两行代码得益于自c;;17以来提供的三个特性:
使用声明打包扩展——使用可变参数模板的简短而紧凑的语法。
自定义模板参数推演规则——允许将lambda对象的列表转换为重载类的基类列表。(注意:在c;;20中不需要!)
聚合初始化的扩展——在c;;17之前;不能聚合初始化派生自其他类型的类型。
在c;; 11中;已经介绍过使用声明的必要性。这对于将调用操作符带入重载结构的相同作用域非常重要。在c;; 17中;得到了一种支持可变参数模板的语法;这在该语言的之前版本中是不可能的。

从lambda表达式派生而来;然后像上一节中那样暴露它们的运算符()。但是如何创建这种重载类型的对象呢?如所知;无法预先知道lambda的类型;因为编译器必须为每个lambda生成一个唯一的类型名。例如;不能这样写:


overload< LambdaType1, LambdaType2> myOverload { ... } // ???

唯一可行的方法是一些make函数(因为模板参数推断适用于函数模板;因为总是):


template <typename... T>
constexpr auto make_overloader(T &&...t)
{
    return overloaded<T...>{std::forward<T>(t)...};
}

使用在c;; 17中添加的模板参数推导规则;可以简化通用模板类型的创建;并且不需要make_overloader函数。例如;对于简单类型;可以这样写:


std:: pair strDouble { std:: string{;Hello; }, 10.0 };
// strDouble is std::pair<std::string, double>

还有一个选项可以定义自定义扣款指南。标准库中大量使用了它们;例如std::array:


template < class T , class... U>
array(T, U...) -> array< T, 1 ; sizeof ...(U)> ;


array test{1 , 2 , 3 , 4 , 5 };
// test is std::array<int, 5>

对于重载模式;可以指定一个自定义的推导指南:


template < class... Ts> overloaded(Ts...) -> overloaded< Ts...> ;

现在;当用两个lambda初始化重载时:


overloaded myOverload { [](int ) { }, [](double ) { } };

这个功能相对简单:现在可以聚合初始化一个派生自其他类型的类型。


struct base1
{
    int b1, b2 = 32;
};
struct base2
{
    base2() { b3 = 64; }
    int b3;
};
struct derived : base1, base2
{
    int d;
};
derived d1{{1, 2}, {}, 4};
derived d2{{}, {}, 4};

代码用 1 初始化 d1.b1;用 2 初始化 d1.b2;用 64 初始化 d1.b3;用 4 初始化 d1.d
.对于第二个对象;代码用 0 初始化 d2.b1;用 32 初始化 d2.b2;用 64 初始化 d2.b3;用 4 初始化 d2.d
在的例子中;它有更重要的影响。因为对于重载类来说;没有聚合初始化;必须实现以下构造函数:


struct overloaded : Fs...
{
    template <class... Ts>
    overloaded(Ts &&...ts) : Fs{std::forward<Ts>(ts)}...
    {
    }
    // ...
}

有了这些知识;就可以使用继承和重载模式来做一些更实际的事情。看一个访问std::variant的例子:


#include <iostream>
#include <variant>
template <class... Ts>
struct overloaded : Ts...
{
    using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main()
{
    const auto PrintVisitor = [](const auto &t)
    { std::cout << t << ; 
 ;; };
    std::variant<int, float, std::string> intFloatString{;Hello;};
    std::visit(PrintVisitor, intFloatString);
    std::visit(overloaded{[](int &i) { i *= 2; },
                          [](float &f) { f *= 2.0f; },
                          [](std::string &s) { s = s ; s; }
            },intFloatString);
    std::visit(PrintVisitor, intFloatString);
}

Lambdas with std::thread
让从c;;11以来就存在的std::thread开始。您可能已经知道;std::thread在其构造函数中接受一个可调用对象。它可能是一个普通的函数指针、一个functor或者一个lambda表达式。一个简单的例子:


#include <iostream>
#include <thread>
#include <vector>
#include <numeric> // for std::iota
int main()
{
    const auto printThreadID = [](const char *str)
    {
        std::cout << str << ;: ;
                  << std::this_thread::get_id() << ; thread id 
 ;;
    };
    std::vector<int> numbers(100);
    std::thread iotaThread([&numbers, &printThreadID](int startArg){
                                std::iota(numbers.begin(), numbers.end(), startArg);
                                printThreadID(;iota in; ); 
                            },10);
    iotaThread.join();
    printThreadID(;printing numbers in;);
    for (const auto &num : numbers)
        std::cout << num << ;, ;;
}




相关文章

  • Clion 2021软件下载和安装教程

    Clion 2021软件下载和安装教程,CLion 2021是款适用于C和C ++的跨平台IDE,功能强大的智能编码辅助和代码分析软件,使用 CLion能够通过即时导航和可靠的重构来提升你的工作效率,......
  • VirtualBox网络连接方式学习笔记

    VirtualBox网络连接方式学习笔记,如果该配置文件命名不一致需要修改,并修改里面内容的名称。桥接模式下,虚拟机和宿主机在同一网段,虚拟机可以访问外网,宿主机可以访问虚拟机,虚拟机可以访问宿主机同一局域网的其他机器。命令查看网卡配置文件,如下可以看到默认配置文件是/etc/sysconfig/network-scripts/这个是VirtualBox的默认设置,不是这个可以改为这个,后续配置文件就不需要改了。NAT模式下,宿主机访问虚拟机,虚拟机可以访问宿主机,虚拟机可以访问互联网。,如果该配置文件命...

网友评论

快盘下载暂未开通留言功能。

关于我们| 广告联络| 联系我们| 网站帮助| 免责声明| 软件发布

Copyright 2019-2029 【快快下载吧】 版权所有 快快下载吧 | 豫ICP备10006759号公安备案:41010502004165

声明: 快快下载吧上的所有软件和资料来源于互联网,仅供学习和研究使用,请测试后自行销毁,如有侵犯你版权的,请来信指出,本站将立即改正。