• Effective Modern C++:06lambda表达式


             lambda表达式实际上是语法糖,任何lambda表达式能做到的,手动都能做到,无非是多打几个字。但是lambda作为一种创建函数对象的手段,实在太过方便,自从有了lambda表达式,使用复杂谓词来调用STL中的”_if”族算法(std::find_if,std::remove_if等)变得非常方便,这种情况同样发生在比较函数的算法族上。在标准库之外,lambda表达式可以临时制作出回调函数、接口适配函数或是语境相关函数的特化版本以供一次性调用。下面是关于lambda相关术语的提醒:

             lambda表达式,是表达式的一种,比如下面代码中红色的就是lambda表达式:

    std::find_if(container.begin(), container.end(),
    [](int val) { return 0 < val && val < 10; });

              闭包,是lambda表达式创建的运行期对象,在上面对std::find_if的调用中,闭包就是作为第三个实参在运行期传递给std::find_if的对象。

             闭包类,是实例化闭包的类,每个lambda表达式都会触发编译器生成一个独一无二的闭包类,而lambda表达式中的语句会变成闭包类成员函数的可执行指令。

            

    闭包可以复制,所以,对应于单独一个lambda表达式的闭包类型可以有多个闭包:

    int x; // x is local variable
    auto c1 = [x](int y) { return x * y > 55; }; // c1 is copy of the closure produced by the lambda
    auto c2 = c1; // c2 is copy of c1
    auto c3 = c2; // c3 is copy of c2

              c1、c2和c3都是同一个lambda表达式产生的闭包的副本。

             在非正式场合,lambda表达式,闭包和闭包类之间的界限可以模糊一些。但是在下面的条款中,需要能区别哪些存在于编译期(lambda表达式和闭包类),哪些存在于运行期(闭包),以及它们之间的相互联系。

    31:避免默认捕获模式

             C++11中有两种默认捕获模式:按引用或按值。按引用的默认捕获模式可能导致空悬引用,而按值的默认捕获模式可能会让你觉得不存在空悬引用的问题(实际上不是)。

             按引用捕获会导致闭包包含指向局部变量(或形参)的引用,一旦由lambda表达式所创建的闭包的生命期超过了该局部变量或形参的生命期,那么闭包内的引用就会空悬,比如下面的代码:

    using FilterContainer = std::vector<std::function<bool(int)>>; 
    FilterContainer filters; // filtering funcs
    
    void addDivisorFilter()
    {
        auto calc1 = computeSomeValue1();
        auto calc2 = computeSomeValue2();
        auto divisor = computeDivisor(calc1, calc2);
        filters.emplace_back(
            [&](int value) { return value % divisor == 0; }
        ); 
    }

              这段代码随时会出错,lambda中按引用捕获了局部变量divisor,但当addDivisorFilter函数返回时局部变量被销毁,使用filters就会产生未定义行为。

             如果不这样做,使用显式方式按引用捕获divisor,问题依然存在:

    filters.emplace_back(
        [&divisor](int value) { return value % divisor == 0; } 
    );

              但是通过显示捕获,就比较容易看出lambda表达式的生存依赖于divisor的生命期。显式的写出”divisor”可以提醒我们要保证divisor至少应该和lambda具有一样长的生命期,这要比[&]这种所传达的不痛不痒的“要保证没有空悬引用”式的劝告更让人印象深刻。

             如果知道闭包会立即使用(比如传递给STL算法)且不会被复制,这种情况下,你可能会争论说,既然没有空悬引用的风险,也就没有必要避免使用默认引用捕获模式。但是从长远观点来看,显示的列出lambda表达式所依赖的局部变量或形参,是更好的软件工程实践。

             上面的例子中,解决问题的一种办法是对divisor采用按值的默认捕获模式:

    filters.emplace_back(
        [=](int value) { return value % divisor == 0; }
    );

              对于这个例子而言,这样做确实是没问题的。但是按值的默认捕获并非一定能避免空悬引用,问题在于如果按值捕获了一个指针,在lambda表达式创建的闭包中持有的是这个指针的副本,但是没有办法阻止lambda表达式之外的代码针对该指针实施delete操作导致的指针副本空悬。比如下面的代码:

    class Widget {
    public:
        … // ctors, etc.
        void addFilter() const; // add an entry to filters
    private:
        int divisor; // used in Widget's filter
    };
    
    void Widget::addFilter() const {
        filters.emplace_back(
            [=](int value) { return value % divisor == 0; }
        );
    }

              这样的代码看起来安全,然而实际上却是大错特错的。捕获只能针对于在创建lambda表达式的作用域内可见的非静态局部变量(包括形参),而在Widget::addFilter函数体内,divisor并非局部变量,而是Widget类的成员变量,它根本没办法捕获。这么一来,如果不使用默认捕获模式,代码就不会通过编译:

    void Widget::addFilter() const {
        filters.emplace_back(
            [](int value) { return value % divisor == 0; }
        );
    }

              而且,如果试图显示捕获divisor(无论是按值还是按引用),这个捕获语句都不能通过编译,因为divisor既不是局部变量,也不是形参:

    void Widget::addFilter() const {
        filters.emplace_back(
            [divisor](int value) { return value % divisor == 0; }
        );
    }

              但是为什么一开始的代码没有发生编译错误呢?this指针是关键所在,每一个非静态成员函数都持有一个this指针,每当提及该类的成员变量时都会用到这个指针。比如在Widget的任何成员函数中,编译器内部都会把divisor替换成this->divisor。因此,在Widget::addFilter的按值默认捕获版本中,被捕获的实际上是Widget的this指针,而不是divisor。从编译器的角度来看,实际的代码相当于:

    void Widget::addFilter() const {
        auto currentObjectPtr = this;
        filters.emplace_back(
            [currentObjectPtr](int value) { return value % currentObjectPtr->divisor == 0; }
        );
    }

              因此,该lambda闭包的存活,与它含有this指针指向的Widget对象的生命期是绑在一起的,比如下面的代码:

    using FilterContainer = std::vector<std::function<bool(int)>>;
    FilterContainer filters; 
    
    void doSomeWork() {
        auto pw = std::make_unique<Widget>(); 
        pw->addFilter();
        …
    }

              当调用doSomeWork时创建了一个筛选函数,它依赖于std::make_unique创建的Widget对象,该函数被添加到filters中,然而当doSomeWork结束后,Widget对象随着std::unique_ptr的销毁而销毁,从那一刻起,filters中就含有了一个带有空悬指针的元素。

             这一问题可以通过将想捕获的成员变量复制到局部变量中,而后捕获该局部变量的部分得意解决:

    void Widget::addFilter() const {
        auto divisorCopy = divisor;
        filters.emplace_back(
            [divisorCopy](int value) { return value % divisorCopy == 0; } 
        );
    }

      

             在C++14中,捕获成员变量的一种更好的方式是使用广义lambda捕获(generalized lambda):

    void Widget::addFilter() const {
        filters.emplace_back(
            [divisor = divisor](int value) { return value % divisor == 0; }
        );
    }

              对广义lambda捕获而言,没有默认捕获模式一说,但是,就算在C++14中,本条款的建议,避免使用默认捕获模式依然成立。

             使用按值默认捕获的另一个缺点,在于它似乎表明闭包是自治的,与闭包外的数据变化绝缘,然而作为一般性的结论,这是不正确的。因为lambda表达式可能不仅依赖于局部变量或形参,他还可能依赖于静态存储期对象,这样的对象定义在全局或名字空间作用域中,或是在类,函数,文件中以static饰词声明。这样的对象可以在lambda内使用,但是它们不能被捕获。如果使用了按值默认捕获模式,这些对象就会给人以错觉,认为它们可以加以捕获:

    void addDivisorFilter() {
        static auto calc1 = computeSomeValue1(); 
        static auto calc2 = computeSomeValue2(); 
        static auto divisor = computeDivisor(calc1, calc2);
        filters.emplace_back(
            [=](int value) { return value % divisor == 0; }
        );
        ++divisor;
    }

              看到[=]就认为lambda复制了它内部使用的对象,得出lambda是自治的这种结论,是错误的。实际上该lambda表达式并不独立,它没有使用任何的非静态局部变量或形参,所以它没能捕获任何东西。更糟糕的是lambda表达式的代码中使用了静态变量divisor,每次调用addDivisorFilter后,divisor会递增,使得添加到filters中的每个lambda表达式的行为都不一样。如果一开始就避免使用按值的默认捕获模式,也就能消除代码被误读的风险了。

    32:使用初始化捕获将对象移入闭包

             有时按值捕获和按引用捕获并不能满足所有的需求。比如想要把move-only对象(如std::unique_ptr或std::future)放入闭包,或者想把复制昂贵而移动低廉的对象移入闭包时,C++11没有提供可行的方法,但是C++14为对象移动提供了直接支持。

             实际上,C++14提供了一种全新的捕获方式,按移动的捕获只不过是该机制能够实现的多种效果之一罢了。这种方式称为初始化捕获(init capture),它可以做到C++11的捕获形式所有能够做到的事情(除了默认捕获模式,而这是需要远离的),不过初始化捕获的语法稍显啰嗦,如果C++11的捕获能解决问题,则大可以使用之。

             下面是初始化捕获实现移入捕获的例子:

    class Widget { 
    public:
        bool isValidated() const;
        bool isProcessed() const;
        bool isArchived() const;
    private:
        …
    };
    auto pw = std::make_unique<Widget>(); 
    … 
    auto func = [pw = std::move(pw)] 
                { return pw->isValidated() && pw->isArchived(); };

              上面的例子中,位于”=”左侧的pw,是lambda创建的闭包类中成员变量的名字;而位于”=”右侧的是其初始化表达式,所以”pw=std::move(pw)”表达了在闭包类中创建一个成员变量pw,然后使用针对局部变量pw实施std::move的结果来初始化该成员变量。在lambda内部使用pw也是指的闭包类的成员变量。一旦定义lambda表达式之后,因为局部变量pw已经被move了,所以其不再掌握任何资源。

             上面的例子还可以不使用局部变量pw:

    auto func = [pw = std::make_unique<Widget>()]
                { return pw->isValidated() && pw->isArchived(); };

              这种捕获方式在C++14中还称为广义lambda捕获(generalized lambda capture)。

             但是如果编译器尚不支持C++14,则该如何实现按移动捕获呢?要知道一个lambda表达式不过是生成一个类并创建一个该类的对象的手法罢了,并不存在lambda能做而手工不能做的事情,上面C++14的例子,如果使用C++11,可以写为:

    class IsValAndArch { 
    public: 
        using DataType = std::unique_ptr<Widget>;
        explicit IsValAndArch(DataType&& ptr) : pw(std::move(ptr)) {}
        bool operator()() const
        { return pw->isValidated() && pw->isArchived(); }
    private:
        DataType pw;
    };
    auto func = IsValAndArch(std::make_unique<Widget>());

              这种写法要比使用lambda麻烦很多。

    如果非要使用lambda实现按移动捕获,也不是全无办法,可以借助std::bind实现:把要捕获的对象移动到std::bind产生的函数对象中;给lambda表达式一个指向欲捕获的对象的引用。比如C++14中的写法:

    std::vector<double> data;  
    … // populate data
    auto func = [data = std::move(data)]
                { /* uses of data */ };

              如果采用C++11中使用std::bind和lambda的写法,等价代码如下:

    std::vector<double> data; 
    … // as above
    auto func = std::bind(
        [](const std::vector<double>& data) { /* uses of data */ },
        std::move(data)
    );

              std::bind也生成函数对象,可以将它生成的对象称为绑定对象。std::bind的第一个实参是个可调用对象,接下来的所有实参表示传递给该对象的值。

             绑定对象内含有传递给std::bind所有实参的副本。对于左值实参,绑定对象内对应的副本实施的是复制构造;对于右值实参,实施的是移动构造。上面的例子中,第二个实参是个右值,所以在绑定对象内,使用局部变量data移动构造其副本,这种移动构造动作正是实现模拟移动捕获的关键所在,因为把右值移入绑定对象,正是绕过C++11无法将右值移动到闭包的手法。

             当一个绑定对象被调用时,它所存储的实参会传递给std::bind的那个可调用对象,也就是func被调用时,func内经由移动构造得到的data副本就会作为实参传递给那个原先传递给std::bind的lambda表达式。这个C++11写法比C++14多了一个形参data,该形参是个指向绑定对象内部的data副本的左值引用,这么一来,在lambda内对data形参所做的操作,都会实施在绑定对象内移动构造而得的data副本之上,与原局部变量data无关。

             默认情况下,lambda闭包类中的operator()成员函数会带有const饰词,因此闭包里的所有成员变量在lambda表达式的函数体内都带有const饰词,但绑定对象内移动构造而得的data副本并不带有const饰词,所以为了防止该data部分在lambda表达式内被意外修改,lambda的形参就声明为常量引用。但是如果lambda表达式带有mutable饰词,则闭包中的operator()函数就不会在声明时带有const饰词,相应的做法就是在lambda声明中略去const:

    auto func = std::bind(
        [](std::vector<double>& data) mutable 
        { /* uses of data */ },
        std::move(data)
    );

              绑定对象存储着传递给std::bind所有实参的副本,因此本例中的绑定对象就包含一份由第一个实参lambda表达式产生的闭包的副本。这么一来,该闭包的生命期就和绑定对象是相同的。

             另外一个例子,下面是C++14的代码:

    auto func = [pw = std::make_unique<Widget>()] 
                { return pw->isValidated() && pw->isArchived(); }; 

              如果使用C++11采用bind的写法:

    auto func = std::bind(
        [](const std::unique_ptr<Widget>& pw)
        { return pw->isValidated() && pw->isArchived(); },
        std::make_unique<Widget>()
    );

      

    33:要对auto&&类型的形参使用std::forward,则需要使用decltype

             泛型lambda表达式(generic lambda)是C++14最振奋人心的特性之一:lambda表达式的形参列表中可以使用auto,它的实现直截了当,闭包类中的operator()采用模板实现。比如下面的lambda表达式,以及其对应的实现:

    auto f = [](auto x){ return func(normalize(x)); };
    
    class SomeCompilerGeneratedClassName {
    public:
        template<typename T> 
        auto operator()(T x) const
        { return func(normalize(x)); }
        …
    }; 

              这个例子中,lambda表达式对x的动作就是将其转发给normalize,如果normalize区别对待左值和右值,则该lambda表达式的实现是有问题的,正确的写法应该是使用万能引用并将其完美转发给normalize:

    auto f = [](auto&& x)
    { return func(normalize(std::forward<???>(x))); };

              这里的问题是,std::forward的模板实参”???”应该怎么写?

    这里可以使用decltype(x),但是decltype(x)产生的结果,却与std::forward的使用惯例有所不同。如果传入的是个左值,则x的类型是左值引用,decltype(x)得到的也是左值引用;如果传入的是右值,则x的类型是右值引用,decltype(x)得到的也是右值引用,但是,std::forward的使用惯例是std::forward<T>,其中T要么是个左值引用,要么是个非引用。

             再看一下条款28中std::forward的简单实现:

    template<typename T> 
    T&& forward(remove_reference_t<T>& param) {
        return static_cast<T&&>(param);
    }

              如果客户代码想要完美转发Widget类型的右值,则按照惯例它应该采用Wdiget类型,而非引用类型来实例化std::forward,然后std::forard模板实例化结果是:

    Widget&& forward(Widget& param) { 
        return static_cast<Widget&&>(param); 
    }

              如果使用右值引用实例化T,也就是Widget&&实例化T,得到的结果是:

    Widget&& && forward(Widget& param) {
        return static_cast<Widget&& &&>(param);
    }

              实施了引用折叠之后:

    Widget&& forward(Widget& param) {
        return static_cast<Widget&&>(param);
    }

              经过对比,发现这个版本和T为Widget时的std::forward是完全一样的,因此,实例化std::forward时,使用一个右值引用和使用非引用类型,结果是相同的。所以,我们的完美转发lambda表达式如下:

    auto f =
        [](auto&& param)
        {
            return func(normalize(std::forward<decltype(param)>(param)));
        };

              稍加改动,就可以得到能接收多个形参的完美转发lambda式版本,因为C++14中的lambda能够接受变长形参:

    auto f =
        [](auto&&... params)
        {
            return func(normalize(std::forward<decltype(params)>(params)...));
        };

      

    34:优先使用lambda表达式,而非std::bind

             std::bind在2005年就已经是标准库的组成部分了(std::tr1::bind),这意味着std::bind已经存在了十多年了,你可能不太愿意放弃这么一个运作良好的工具,然而有时候改变也是有益的,因为在C++11中,相对于std::bind,lambda几乎总会是更好的选择,而到了C++14,lambda简直已成了不二之选。

             lambda表达式相对于std::bind的优势,最主要的是其具备更高的可读性:

    // typedef for a point in time (see Item 9 for syntax)
    using Time = std::chrono::steady_clock::time_point;
    // see Item 10 for "enum class"
    enum class Sound { Beep, Siren, Whistle };
    // typedef for a length of time
    using Duration = std::chrono::steady_clock::duration;
    // at time t, make sound s for duration d
    void setAlarm(Time t, Sound s, Duration d);
    
    // setSoundL ("L" for "lambda") is a function object allowing a
    // sound to be specified for a 30-sec alarm to go off an hour
    // after it's set
    auto setSoundL = [](Sound s)
        {
            // make std::chrono components available w/o qualification
            using namespace std::chrono;
            setAlarm(steady_clock::now() + hours(1), s, seconds(30));
        };

              这里的lambda表达式,即使是没什么经验的读者也能看出来,传递给lambda的形参会作为实参传递给setAlarm。到了C++14中,C++14提供了秒,毫秒和小时的标准字面值,所以,可以写成这样:

    auto setSoundL =
    [](Sound s)
    {
        using namespace std::chrono;
        using namespace std::literals;
        setAlarm(steady_clock::now() + 1h, s, 30s);
    };

              而下面的代码是使用std::bind的等价版本,不过实际上它还有一处错误的,后续在解决这个错误:

    using namespace std::chrono;
    using namespace std::literals;
    using namespace std::placeholders; // needed for use of "_1"
    auto setSoundB = // "B" for "bind"
        std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);

              对于初学者而言,占位符”_1”简直好比天书,而即使是行家也需要脑补出从占位符数字到它在std::bind形参列表中的位置映射关系,才能理解在调用setSoundB时传入的第一个实参,会作为第二个实参传递给setAlarm。该实参的类型在std::bind的调用过程中是未加识别的,所以还需要查看setAlarm的声明才能决定应该传递何种类型的实参到setSoundB。

             这段代码的错误之处在于,在lambda表达式中,表达式”steady_clock::now() + 1h”是setAlarm的实参之一,这一点清清楚楚,该表达式会在setAlarm被调用时求值,这样是符合需求的,就是需要在setAlarm被调用的时刻之后的一个小时启动报警。但是在std::bind中,”steady_clock::now() + 1h”作为实参传递给std::bind,而非setAlarm,该表达式在调用std::bind时就进行求值了,并且求得的结果会存储在绑定对象中,这导致的结果是报警的启动时刻是在std::bind调用之后的一个小时,而非setAlarm调用之后的一个小时。

             要解决这个问题,就需要std::bind延迟表达式的求值到调用setAlarm的时刻,实现这一点,就是需要嵌套第二层std::bind的调用:

    auto setSoundB =
        std::bind(setAlarm,
                  std::bind(std::plus<>(), steady_clock::now(), 1h),
                  _1,
                  30s);

              在C++14中,标准运算符模板的模板类型实参大多数情况下可以省略不写,所以此处也没必要在std::plus中提供了,而C++11中还没有这样的特性,所以在C++11中,想要实现上面的代码,只能是:

    using namespace std::chrono; // as above
    using namespace std::placeholders;
    auto setSoundB =
        std::bind(setAlarm,
             std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)),
             _1,
             seconds(30));

      

             如果对setAlarm实施了重载,则又会有新的问题:

    enum class Volume { Normal, Loud, LoudPlusPlus };
    void setAlarm(Time t, Sound s, Duration d, Volume v);
    auto setSoundL = 
    [](Sound s)
    {
        using namespace std::chrono;
        setAlarm(steady_clock::now() + 1h, s, 30s);
    };

              即使有了重载,lambda表达式依然能正常工作,重载决议会选择有三个参数版本的setAlarm。但是到了std::bind,就没办法通过编译了:

    auto setSoundB = 
        std::bind(setAlarm, 
                std::bind(std::plus<>(),steady_clock::now(),1h),
                _1,
                30s);

              这是因为编译器无法确定应该将哪个setalarm传递给set::bind,它拿到的所有信息只有一个函数名。为了使std::bind能够通过编译,setAlarm必须强制转换到适当的函数指针类型:

    using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
    auto setSoundB = 
        std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
                std::bind(std::plus<>(), steady_clock::now(), 1h),
                _1,
                30s);

              但是这么做又带来了lambda和std::bind的另一个不同之处。在lambda生成的setSoundL的函数调用运算符中,调用setAlarm采用的是常规函数唤起方式,这么一来,编译器就可以用惯常的手法将其内联:

    setSoundL(Sound::Siren); // body of setAlarm may well be inlined here

              而std::bind调用中使用了函数指针,这意味着在setSoundB的函数调用运算符中,setAlarm是通过函数指针来调用的,编译器一般无法将函数指针发起的函数调用进行内联,所以lambda表达式就有可能生成比std::bind更快的代码。

             在setAlarm例子中,仅仅涉及了函数的调用而已,如果你想做的事比这更复杂,则lambda表达式的优势则更加明显。比如:

    auto betweenL =
        [lowVal, highVal]
        (const auto& val)
        { return lowVal <= val && val <= highVal; };

              这里的lambda使用了捕获。std::bind要想要实现同样的功能,必须用比较晦涩的方式来构造代码,下面分别是C++14和C++11的写法:

    using namespace std::placeholders;
    auto betweenB =
        std::bind(std::logical_and<>(),
            std::bind(std::less_equal<>(), lowVal, _1),
            std::bind(std::less_equal<>(), _1, highVal));
    
    auto betweenB = 
        std::bind(std::logical_and<bool>(),
            std::bind(std::less_equal<int>(), lowVal, _1),
            std::bind(std::less_equal<int>(), _1, highVal));

              还是需要使用std::bind的延迟计算方法。

            

             再看下面的代码:

    enum class CompLevel { Low, Normal, High }; 
    Widget compress(const Widget& w, CompLevel lev); //make compressedcopy of w
    Widget w;
    using namespace std::placeholders;
    auto compressRateB = std::bind(compress, w, _1);

              这里的w传递给std::bind时,是按值存储在std::bind生成的对象中的,在std::bind的调用中,按值还是按引用存储只能是牢记规则。std::bind总是复制其实参,但是调用方可以通过对实参实施std::ref的方法达到按引用存储的效果,因此:

    auto compressRateB = std::bind(compress, std::ref(w), _1);

     结果就是compressRateB的行为如同持有的是个指向w的引用,而非其副本。

    而在lambda中,w无论是按值还是按引用捕获,代码中的书写方式都很明显:

    auto compressRateL = 
    [w](CompLevel lev)
    { return compress(w, lev); };

              同样明显的还有形参的传递方式:

    compressRateL(CompLevel::High); // arg is passed by value
    compressRateB(CompLevel::High); // how is arg passed?

              Lambda返回的闭包中,很明显实参是按值传递给lev的;而在std::bind返回绑定对象中,形参的传递方式是什么呢?这里也只能牢记规则,绑定对象的所有实参都是按引用传递的,因为此种对象的函数调用运算符使用了完美转发。

            

             总而言之,lambda表达式要比std::bind可读性更好,表达能力更强,运行效率也可能更好,在C++14中,几乎没有std::bind的适当用例,而在C++11中,std::bind仅在两个受限场合还算有使用的理由:

             移动捕获,C++11没有提供移动捕获的语法,参考上一条款;

             多态函数对象,因为绑定对象的函数调用运算符使用了完美转发,所以可以接收任何类型的实参,因此当需要绑定的对象具有一个函数调用运算符模板时,是有利用价值的:

    class PolyWidget {
    public:
        template<typename T>
        void operator()(const T& param);
        …
    };
    
    PolyWidget pw;
    auto boundPW = std::bind(pw, _1);
    boundPW(1930); // pass int to PolyWidget::operator()
    boundPW(nullptr); // pass nullptr to PolyWidget::operator()
    boundPW("Rosebud"); // pass string literal to PolyWidget::operator()

              C++11中的lambda表达式没有办法实现这一点,但是在C++14中,使用带有auto类型形参的lambda表达式可以很容易的实现这一点:

    auto boundPW = 
    [pw](const auto& param) 
    { pw(param); };
  • 相关阅读:
    poj2928:素数回文数的个数
    R语言学习中的小bug:R中矩阵相乘错误于A %*% B: 需要数值/复数矩阵/矢量参数
    poj3247:回文素数
    Python爬虫之BeautifulSoap的用法
    python jieba库的使用说明
    彻底弄懂python编码
    第八周助教总结
    python中数组用法
    python列表操作大全
    python—各种常用函数及库
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/9937013.html
Copyright © 2020-2023  润新知