奇异递归模板模式(CRTP)应用--表达式模板(expression template) 2

2023-05-16

1 表达式模板(expression template)概述

首先分几个部分介绍下expression template。

1.1 表达式模板(expression template)是什么?

引用wiki 介绍的 Expression templates :

Expression templates are a C++ template metaprogramming technique that builds structures representing a computation at compile time, which structures are evaluated only as needed to produce efficient code for the entire computation. Expression templates thus allow programmers to bypass the normal order of evaluation of the C++ language and achieve optimizations such as loop fusion.
Expression templates were invented independently by Todd Veldhuizen and David Vandevoorde; it was Veldhuizen who gave them their name.They are a popular technique for the implementation of linear algebra software.

结合上面的介绍,简单总结下expression template,表达式模板(expression-template)是一种C++模板元编程技术,它在编译时刻构建好一些能够表达某一计算的结构,对于整个计算这些结构仅在需要(惰性计算、延迟计算)的时候才产生相关有效的代码运算。因此,表达式模板允许程序员绕过正常(这里不知如何翻译,其实可以理解写程序的一般做法吧)的C++语言计算顺序,从而达到优化计算的目的,如循环合并。Expression template是Todd Veldhuizen & David Vandevoorde 发明的,详情见其技术报告:Veldhuizen, Todd (1995). “Expression Templates”. C++ Report.。expression template技术在线性代数相关软件中应用十分广泛,比如Eigen、 Boost uBLAS等等。

1.2 为什么引入 expression template

先通过一个例子来说明,假设在机器学习中我们现在需要构造一个类,这个类有size_ 维feature,每个feature的值是个double类型的实数,这个类不同对象间的同一维度的feature可以进行简单的算术运算(比如+、- maximum…),比较简单的思想是进行运算符重载,具体见代码:

// a naive solution( bad! )
// operator overloading
#include <iostream>
#include <algorithm>
#include <cassert>

using namespace std;

class MyType {
public:
    MyType(size_t size, double val = 0.0) : size_(size) {
        features_ = new double [size_];
        for (size_t i = 0; i < size_; i++) features_[i] = val;
        cout << "\tMyType constructor size = " << size_ << "\n";
    }

    // construct vector using initializer list 
    MyType(std::initializer_list<double> init) {
        size_ = init.size();
        features_ = new double[size_];
        for (size_t i = 0; i < size_; i++) features_[i] = *(init.begin() + i);
            cout << "\tMyType constructor size = " << size_ << "\n";
    }
    // in c++11 move constructor can be used here 
    MyType(const MyType& rhs):size_(rhs.size()) {
        features_ = new double[size_];
        for (size_t i = 0; i < size_; i++) features_[i] = rhs[i];
        cout << "\tMyType copy constructor size = " << size_ << "\n";
    }

    MyType& operator=(const MyType& rhs) {
        if (this != &rhs) {
            delete[]features_;
            size_ = rhs.size();
            features_ = new double[size_];
            for (size_t i = 0; i < size_; i++) features_[i] = rhs[i];
            cout << "\tMyType assignment constructor size = " << size_ << "\n";
        }
        return *this;
    }                  

    ~MyType() { 
        if (nullptr != features_) {
            delete [] features_;
            size_ = 0;
            features_ = nullptr;
        }
    }

    double &operator[](size_t i) { return features_[i]; }
    double operator[](size_t i) const { return features_[i]; }
    size_t size()               const { return size_; }
private:
    size_t size_;
    double* features_;
};

//This kind of approach is inefficient, because temporal memory is allocated and de-allocated during each operation
MyType operator+(MyType const &u, MyType const &v) {
    MyType sum(std::max(u.size(), v.size()));
    for (size_t i = 0; i < u.size(); i++) {
        sum[i] = u[i] + v[i];
    }
    cout << "\t in MyType +\n";
    return sum;
}
// operator- balabala

int main() {
    MyType a(3, 1.1);
    MyType b(3, 2.01);
    MyType c = {3.01, 3.01, 3.01};

    cout << "\t----computing-----\n";
    MyType d = a + b + c;
    for (size_t i = 0; i < d.size(); i++) {
        cout << "\t" << d[i] << "\n";
    }

    return 0;
}

因为我们这个类涉及资源申请,因此需要实现普通构造函数、拷贝构造、赋值构造、析构(其实C++11里实现移动构造、移动赋值更佳,后面会code说明)。运行结果如下:
naive operator loading
对于实现d=a+b+c这样的多个连加的表达式,我们可以看到它进行多次对象的构造(对象a、b、c进行三次普通对象构造(必需),然后按照+的顺序进行运算,按照计算顺序,表达式其实是这样的d=((a+b)+c)),首选a+b相加会构造一个临时对象,a、b相加的结果(这里假设a+b=tmp1)返会又要进行拷贝构造,然后表达式实际变成d=tmp1+c, tmp1和c相加又会构造一个临时对象(这里假设为tmp2,即有tmp1+c=tmp2),然后tmp2拷贝构造生成最终的结果d,细数下简单的连加进行了很多对象的构造析构(其实就是很多次内存的申请和释放,这个效率不要太低!!!)。仿佛听到有童鞋高呼我们可以用C++11的移动语义,来减少临时对象(内存的频繁申请、释放)频繁内存操作,好先上代码:

    // move constructor
    MyType(MyType&& rhs) :size_(rhs.size()) {
        features_ = rhs.features_;
        rhs.size_ = 0;
        rhs.features_ = nullptr;
        cout << "\tMyType move constructor size = " << size_ << "\n";
    }
    // move assignment
    MyType& operator=(MyType&& rhs) {
        if (this != &rhs) {
            size_ = rhs.size_;
            rhs.size_ = 0;
            features_ = rhs.features_;
            rhs.features_ = nullptr;
            cout << "\tMyType move assignment constructor size = " << size_ << "\n";
        }
        *this;
    }

即是利用C++11的移动语义(move constructor 、assignment constructor )来减少内存的重复申请与释放,然而对于d = a + b + c 这样连加的case至少需要有一次临时内存的申请与释放和两次加操作的循环(如下图,标号1的内存即为a+b=tmp1构造过程,对象tmp1存放临时结果,当tmp1与c相加即有tmp1+c=tmp2,再生成tmp2后tmp1便释放内存,tmp2用移动构造出连加结果d),因此,下面不得不引入一个大杀器 expression template。
naive2

注意 上面几张图的结果是在vs2015上编译的结果,可见没进行NRVO(named return value optimization),进行了RVO即Return Value Optimization,是一种编译器优化技术,可以把通过函数返回创建的临时对象给”去掉”,然后可以达到少调用拷贝构造的操作。号外,这篇有介绍 RVO和std::move的原理 的文章,可以看下。所以采用不同的编译器以及优化等级可能结果和上图不同, g++ 默认是开启了 NRVO,可以加上这个编译参数-fno-elide-constructors 去掉NRVO,详细的测试可以参见,下面给出的github地址。

1.3 expression template 想解决的问题

引用wiki More C++ Idioms/Expression-template 所说的Express Template的意图:

Intent

  • To create a domain-specific embedded language (DSEL) in C++.
  • To support lazy evaluation of C++ expressions (e.g., mathematical expressions), which can be executed much later in the program from the point of their definition.
  • To pass an expression – not the result of the expression – as a parameter to a function.

总结来说其实Expression Template想要解决的问题是:
1. 在C++中创建一个嵌入式领域专用语言(DSEL)(大概意思就是在一个编程语言里嵌入一个领域特定语言。参考 这个 Domain Specific Languages in C++ )。
2. 支持表达式的懒惰计算(延迟计算),相对其定义它可以推迟表达式的计算。
3. 传递一个表达式,不是表达式的结果而是把表达式自身作为一个参数传给一个函数。

1.4 expression template的方法

结合1.2中的naive operator overloading下面举例说明表达式模板的神奇功效,先show代码(对CRTP技术不了解的请先看我之前写的奇异递归模板模式( Curiously Recurring Template Pattern,CRTP)1)

// expression template
// Copyright (C) 2017 http://ustcdane.github.io/

#include <algorithm>
#include <cstdio>
#include <cassert>

// this is expression, all expressions must inherit it
template<typename A>
struct Expression {
    // returns const reference of the actual type of this expression
        const A& cast() const { return static_cast<const A&>(*this); }
        int size() const { return cast().size(); }// get Expression size
private:
        Expression& operator=(const Expression&) {
            return *this;
        }
        Expression() {}
        friend A;
};

// binary expression: (binary op,lhs, rhs)
// Func can easily allows user customized theirs binary operators
template<typename Func, typename TLhs, typename TRhs>
struct BinaryOp : public Expression<BinaryOp<Func, TLhs, TRhs> > {
    BinaryOp(const TLhs& lhs, const TRhs& rhs):lhs_(lhs.cast()), rhs_(rhs.cast()) {}

    // work function, computing this expression at position i, import for lazy computing
    double value(int i) const {
        return Func::Op(lhs_.value(i), rhs_.value(i));
    }

    int size() const { return std::max(lhs_.size(), rhs_.size()); }

private:
    const TLhs& lhs_;
    const TRhs& rhs_;
};

// template binary operation, works for any expressions
template<typename Func, typename TLhs, typename TRhs>
inline BinaryOp<Func, TLhs, TRhs>
expToBinaryOp(const Expression<TLhs>& lhs, const Expression<TRhs>& rhs) {
    return BinaryOp<Func, TLhs, TRhs>(lhs.cast(), rhs.cast());
}

// binary operators +
struct Add {
    // any function defined inside its class definition is inline
    static double Op(double a, double b) { 
        return a + b;
    }
};

// define Minimum
struct Minimum {
    static double Op(double a, double b) {
        return a < b ? a : b;
    }
};

template<typename TLhs, typename TRhs>
inline BinaryOp<Add, TLhs, TRhs>
operator+(const Expression<TLhs>& lhs, const Expression<TRhs>& rhs) {
    return expToBinaryOp<Add>(lhs, rhs);
}

template<typename TLhs, typename TRhs>
inline BinaryOp<Minimum, TLhs, TRhs>
min(const Expression<TLhs>& lhs, const Expression<TRhs>& rhs) {
    return expToBinaryOp<Minimum>(lhs, rhs);
}

// allocation just by user
// no constructor and destructor to allocate and de-allocate memory
class MyExpType : public Expression<MyExpType> {
public:
    MyExpType():size_(0) {}
    MyExpType(double *features, int size)
        : size_(size), features_(features) {
        printf("MyExpType constructor size = %d. No memory allocate.\n", size_);
    }

    // delete copy constructor, 
    MyExpType(const MyExpType& src_) = delete;
    template<typename ExpType>
    MyExpType(const Expression<ExpType>& src_) = delete;

    // here is where computing happens,lazy support
    template<typename ExpType>
    MyExpType& operator=(const Expression<ExpType>& src) { 
        const ExpType &srcReal = src.cast();
        assert(size_ >= srcReal.size()); //
        for (int i = 0; i < srcReal.size(); ++i) {
            features_[i] = srcReal.value(i); // binary expression value work function
        }
        printf("MyExpType assignment constructor size = %d\n", size_);
        return *this;
    }
    // computing function
    double value(int i) const { return features_[i]; }
    int size()      const { return size_; }

private:
    int size_;
    double* features_;
};

void print(const MyExpType& m) {
    printf("( ");
    for (int i = 0; i < m.size() - 1; ++i) {
        printf("%g, ", m.value(i));
    }
    if (m.size()) 
        printf("%g )\n", m.value(m.size()-1));
    else 
        printf(" )\n");
}

int main() {
    const int N = 3;
    double sa[N] = { 1.1,1.1, 1.1 };
    double sb[N] = { 2.01, 2.01, 2.01 };
    double sc[N] = { 3.01, 3.01, 3.01 };
    double sd[N] = { 0 };
    MyExpType A(sa, N), B(sb, N), C(sc, N), D(sd, N);
    printf("\n");
    printf(" A = "); print(A);
    printf(" B = "); print(B);
    printf(" C = "); print(C);
    printf("\n\tD = A + B + C\n");
    D = A + B + C;
    for (int i = 0; i < A.size(); ++i) {
    printf("%d:\t%g + %g + %g = %g\n",
    i, B.value(i), C.value(i), B.value(i), D.value(i));
    }
    printf("\n\tD = A + min(B, C)\n");
    // D = A + expToBinaryOp<Minimum>(B, C);
    D = A + min(B, C);
    for (int i = 0; i < A.size(); ++i) {
    printf("%d:\t%g + min(%g, %g) = %g\n",
    i, A.value(i), B.value(i), C.value(i), D.value(i));
    }

    return 0;
}

程序运行结果如下图:

这里写图片描述

如上图所示,看到expression template的神奇了吧,我们的类这下没有内存申请、释放(用户给一个有效地址内存及size即可),三连加只有一次赋值构造,无内存申请、释放,而且只有一次循环。正像上一小节所说的,Expression template所要解决是延迟计算问题(lazy evaluation )。它可以在C++中让operator+返回自定义类型来实现这一功能。当表达式比较长时可以有效的构建出表达式树 如分析树(parse tree)这样的形式 。对应到本程序的表示式 D=A+B+C ,其对应的表达(类型)树如下图所示,表达式的计算只在赋值(operator=)给一个实际对象时才进行。表达式模板通过在编译期的表达式树来实现延迟计算。

expression trees

表达式右侧被解析为类型expToBinaryOp<expToBinaryOp<MyExpType, MyExpType>, MyExpType > 这里记为RHS,如表达式树所示,表达式树结点的类型是在编译期(并且code inlining)从底向上传递的。当调用类型MyExpType 赋值运算符时,这里只展开一次循环,即在位置i处会调用RHS.value(i),类型RHS会根据operator+ 和value(i)的定义递归的把RHS.value(i)展开为 RHS.value(i) = A.value(i) + B.value(i) + C.value(i),可以看到表达式的神奇,无内存申请释放,多次循环变成一次循环,即如下代码:

for (int i = 0; i < D.size(); ++i) {
    D.features_[i] = A.value(i) + B.value(i) + C.value(i);
} 

2. 表达式模板demo

结合表达式模板的相关分析,实现了一个支持一元及二元自定义类型的表达式模板操作库(当然是用了CRTP技术),来段测试代码先:

#include <iostream>
#include <cmath>

#include "valueType.hpp"

using namespace std;
using op::doubleType;

doubleType algorithm(const doubleType x[2]) {
    doubleType y = 1;
    y += x[1] + x[0] * x[1];
    cout << "doubleType y = 1 + x[1] + x[0]*x[1]\n";
    return y;
}

doubleType algorithm2(const doubleType x[2]) {
    doubleType y = exp(x[0]) + -x[1] + 1;
    cout << "doubleType y = exp(x[0]) + -x[1] + 1\n";
    return y;
}

doubleType algorithm3(const doubleType x[2]) {
    const double PI = 3.141592653589793;
    doubleType y = cos(x[0]) + sin(PI/2*log2(x[1]));
    cout << "cos(x[0]) + sin(PI/2*log2(x[1]))\n";
    return y;
}

doubleType algorithm4(const doubleType x[2]) {
    const doubleType m = -0.618;
    doubleType y = min(m, min(x[0], x[1]));
    cout << "m=-0.618\nmin(m, min(x[0], x[1]))\n";
    return y;
}

int main(int argc, char** argv)
{
    doubleType x[2]; 
    doubleType y;    

    x[0] = 0.0;
    x[1] = 2.0;
    cout << "x[0] = " << x[0] << endl;
    cout << "x[1] = " << x[1] << endl;
    std::cout << "Begin run algorithm:\n";

    y = algorithm(x);
    std::cout << "y = " << y.value() << "\n";

    y = algorithm2(x);
    std::cout << "y = " << value(y) << "\n";

    y = algorithm3(x);
    std::cout << "y = " << y << "\n";
    if (2 == y)
        cout << "Yes.\ty = 2\n";

    y = algorithm4(x);
    std::cout << "y = " << value(y) << "\n";

    return 0;
}

执行结果:
exp_op

分析:如代码中 algorithm3 y=cos(x[0]) +sin(PI/2*log2(x[1])),其对应的表达式类型树如下图所示:

tree2

具体为什么是这样的可以参考代码,其中ScalarMul是表达式类型和标量类型相乘的表达式类型。本文所有代码请到我的github。

3. Expression Template总结

expression template能让代码高效很多,即使现在最新的cpp11移动语义也不能达到我们所说的表达式模板连加的效率,尤其在代数计算领域,表达式模板更是实现相关工具的典范,有机会的话再写篇关于自动求导(automatic differentiation,AD)的文章吧(好像介绍CRTP技术的时候就说要写了…),也是利用了表达式模板的技术。

参考:

  1. wiki Expression_templates
  2. More C++ Idioms/Expression-template
  3. Modern C++ design
  4. mshadow Expression Template
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

奇异递归模板模式(CRTP)应用--表达式模板(expression template) 2 的相关文章

  • 3D Slicer源代码编译与调试(Visual Studio)

    开始 本文将Slicer的源码在Windows系统的编译过程记录下来 我的编译环境 xff1a Qt5 9 3VS2015Git 2 16 1CMake 3 14 1NSIS Unicode NSIS 编译 上述编译环境的准备好之后 xff
  • c++对象模型

    一 什么是c 43 43 对象模型 语言中直接支持面向对象程序设计的部分 对于各种支持的底层实现机制 二 c 43 43 对象的布局成本 成员函数不占用成本 member functions虽然再class的声明之内 xff0c 却不在ob
  • mybatis-plus返回查询总记录数方式(亲测)

    这篇文章主要介绍了mybatis plus返回查询总记录数方式 xff0c 具有很好的参考价值 xff0c 希望对大家有所帮助 如有错误或未考虑完全的地方 xff0c 望不吝赐教 mybatis plus返回查询总记录数mybatis pl
  • Android 模拟器串口与PC虚拟串口通讯

    基于上一篇文章 xff0c Android studio 使用NDK 实现串口 动态库 使用NDK生成 so 库操作PC中的串口 以及Android studio 3 0 and Gradle3 0 JNI 生成 so 库 1 开发环境 1
  • ArchLinux遇到问题unable to lock database

    在ArchLinux上更新系统或者安装软件 xff0c 如 pacman Syu xff0c 遇到下列问题 xff1a error failed to init transaction unable to lock database err
  • 【终极解决方案】当前不会命中断点,还没有为该文档加载任何符号

    我们在用vs进行debug时 xff0c 有的时候会出现无法单步调试 xff0c 提示 当前不会命中断点 xff0c 还没有为该文档加载任何符号 查询网上资料 xff0c 基本都是以下这样 xff1a 在vs里边 xff0c 工具 gt 选
  • Gitlab的安装及使用

    1 Gitlab概述 1 1 GitLab介绍 GitLab是利用Ruby on Rails一个开源的版本管理系统 xff0c 实现一个自托管的Git项目仓库 xff0c 可通过Web界面进行访问公开的或者私人项目 GitLab能够浏览源代
  • UVa 11168 Airport

    这个月看看计算几何 xff0c 这道题写的代码出现的问题还是挺多的 xff0c 不过索性最后解决了 题意 xff1a 给出平面上n个点 xff0c 找一条直线 xff0c 使得所有点在直线的同侧 xff08 也可以在直线上 xff09 xf
  • 使用JSON实现SQL Server少量数据传递(导入导出)

    摘要 xff1a 在开发业务系统时 xff0c 对于使用SQL Server作为业务数据库时 xff0c 要将数据从发库数据配置完成后需要同步到生产库时 xff0c 使用SQL Server自带的工具不是那么流畅 xff0c 本文介绍一种使
  • Mybatis Plus中的lambdaQueryWrapper和LambdaUpdateWrapper

    一 Mybatis Plus中的lambdaQueryWrapper xff1a 用法 xff1a 1 mybatis plus依赖中接口类IService中有这样一个方法 default List lt T gt list Wrapper
  • Docker 从Dockerfile 构建镜像 :build 命令的用法

    前些天发现了一个巨牛的人工智能学习网站 xff0c 通俗易懂 xff0c 风趣幽默 xff0c 忍不住分享一下给大家 点击跳转到教程 Dockerfile 创建完成后 xff0c 可以使用 docker build 命令根据 Dockerf
  • Windows下subsystem子系统(wsl)的默认安装位置

    位置在 xff1a C Users THINK AppData Local Packages CanonicalGroupLimited UbuntuonWindows 79rhkp1fndgsc LocalState rootfs 备注
  • 关于提示unrecognized class file version的原因

    今天在发布一个网站的时候 xff0c 发现用java jar 可以运行jar包 xff0c 但是运行到一半会卡住如下图所示 但是我在idea里却可以完美的运行 xff0c 于是我去查了一下资料 xff0c 然后改了几个配置 xff0c 最后
  • Hex编码

    Hex编码的编码原理 xff1a Hex编码的原理就是将原来8位的二进制字节打断 xff0c 分成两个4位的 xff0c 并且在前面加上4个零 xff0c 进行补位这样一个8位二进制字节就变成了2个8位的二进制字节 xff0c 在将新得到的
  • request.post()

    在通过requests post 进行POST请求时 xff0c 传入报文的参数有两个 xff0c 一个是data xff0c 一个是json 常见的form表单可以直接使用data参数进行报文提交 xff0c 而data的对象则是pyth
  • 将python项目(django/flask)打包成exe和安装包

    目录 打包Flask项目写一个简单的Flask项目下载pyinstaller进入到项目路径下 xff0c 执行运行exe xff0c 测试 使用nsis把文件夹打包成windows的安装包下载安装nsis把dist文件夹下的run文件夹压缩
  • Kotlin学习

    开始学习Kotlin xff0c 将学习过程中遇到的 xff0c 看到的知识点记录下来 配置Kotlin环境 在项目根目录build gradle配置 buildscript ext kotlin version 61 39 1 2 71
  • tortoiseGit使用报错gitlab ssh Please make sure you have the correct access rights and the repos

    1 报错现象 xff1a Please make sure you have the correct access rights and the repository exists 2 背景 使用git连接下载公司gitlab项目 xff0
  • 计算几何模板

    初始定义 xff1a xff08 基本都要用到 xff0c 做题直接全打上 xff09 struct Point double x y Point double x 61 0 double y 61 0 x x y y 构造函数 xff0c
  • 【Linux】【踩坑专栏】centOS 7 桌面版使用VMWare Workstation Player 16安装时踩坑

    CentOS 7 Everything在用VMWare Workstation Player 16安装时 xff0c 第二个界面是下面这个 xff1a 我安装的时候误以为 全名 是个性化Linux的全名 xff0c 其实指的是用户的全名 x

随机推荐

  • C#怎样实现远程连接SQL Server2005

    本文详细讲述了C 怎样实现远程连接SQL Server2005各个步骤 首先配置SQLSERVER2005 xff1a 打开 Microsoft SQL Server Management Studio 直接用Windows 用户连接进入
  • 九、Python第九课——Python中的if语句与运用

    xff08 请先看置顶博文 xff09 https blog csdn net GenuineMonster article details 104495419 目录 一 if语句 1 检查变量存储的值是否相等 2 判定字母或字符串时区别大
  • windows环境下wampserver的配置教程

    对 于初做 PHP 网站的朋友来说 xff0c 第一步肯定是希望在自己电脑是搭建 PHP 环境 xff0c 省去空间和上传的麻烦 xff01 但搭建环境也不是件容易的事情 xff0c 特别是对于新手同学来 说 xff01 因此在这里跟大家介
  • CentOS7.X 新装后个性化处理

    目录 1 修改HostName2 Shell脚本中文乱码问题解决3 部分常用开发工具安装4 常用开发工具安装PS 1 修改HostName 设置自定义HostName hostnamectl set hostname MyHostName
  • ffmpeg命令行提示“no such file or directory...”

    最近参考一博客测试使用ffmpeg将rtsp流分片成ts文件 xff0c 附上大神博客链接 xff0c 很有用 xff01 xff01 xff01 https blog csdn net kunzai6 article details 76
  • Python pygame安装过程笔记

    分享一下我老师大神的人工智能教程 xff01 零基础 xff0c 通俗易懂 xff01 http blog csdn net jiangjunshow 也欢迎大家转载本篇文章 分享知识 xff0c 造福人民 xff0c 实现我们中华民族伟大
  • PyQtgraph结合Pyside6绘图解决pyqtgraph模块无GraphicsWindow的问题

    解决前辈的示例的问题 发生异常 AttributeError module pyqtgraph has no attribute GraphicsWindow 代码如下 xff1a 将 xff1a win 61 pg GraphicsWin
  • Proteus&keil-51单片机-外部中断控制流水灯

    实现功能 利用P0端口进行花样显示 xff0c 显示顺序为 xff1a 8个LED灯依次左移点亮 xff1b 8个LED灯依次右移点亮 xff1b xff0c LED0 LED2 LED4 LED6亮1秒熄灭 xff0c LED1 LED3
  • 51单片机-60秒计时

    span class token macro property span class token directive hash span span class token directive keyword include span spa
  • 关于计算几何某些定理·基础知识的汇总

    欧拉定理 xff1a 设平面图的顶点数 xff0c 边数和面数分别为V xff0c E和F xff0c 则V 43 F E 61 2 直线方程两点式转换为一般式 xff1a 1 两点式 xff1a y y2 y1 y2 61 x x2 x1
  • C51单片机和ADC0832芯片设计数字电压表

    span class token macro property span class token directive hash span span class token directive keyword include span spa
  • opencv for python绘制箭靶并标注环数

    先从外到内循环绘制圆 再添加数字 有待改进 最后绘制十字线 span class token comment 绘制箭靶并标注环数 span span class token keyword import span cv2 span clas
  • opencv for python 绘制圆角矩形

    span class token comment 绘制100 240像素 圆角20的矩形 span span class token keyword import span cv2 span class token keyword as s
  • opencv鼠标指针左键画图,右键清除.

    span class token comment 按住鼠标左键画图 双击鼠标左键可以清除 span span class token keyword import span cv2 span class token keyword as s
  • macOS命令释放可释放空间(不用CleanMyMac)

    背景 众所周知 xff0c CleanMyMac的 释放可清除空间 功能非常厉害 xff0c 在用户明明已经删除了大量文件腾出几十G空间的情况下 xff0c macOS的存储管理里面仍然会显示可用空间不足 xff0c 甚至升级大型软件会提示
  • 使用 PyInstaller 把python程序 .py转为 .exe 可执行程序

    最近使用Python为项目开发一款绘图工具 绘出 声场三维模型 因为希望能把Python脚本发布为脱离Python平台运行的可执行程序 xff0c 比如单个 的exe文件 PyInstaller恰满足这个需求 本文PyInstaller的版
  • 字符串最小周期串问题

    问题描述 xff1a 如果一个字符串可以由某个长度为n的字符串重复多次得到 xff0c 则该串以n为周期 例如 xff0c abcabcabcabc以3为周期 xff08 注意 xff0c 它也以6和12为周期 xff09 输入一个长度不超
  • linux 下使用 rsync 进行文件 同步

    rsync 介绍 rsync是类unix系统下的数据镜像备份工具 remote sync rsync是一个功能非常强大的工具 xff0c 其命令也有很多功能特色选项 xff0c 我们下面就对它的选项一一进行分析说明 它的特性如下 xff1a
  • linux 下安装、使用 redis

    redis介绍 Redis是一个开源 支持网络 基于内存 键值对存储数据库 xff0c 使用ANSI C编写 xff0c redis中文官方网站 xff0c 点这里 redis安装 我的linux操作系统为ubuntu12 04 登录 ht
  • 奇异递归模板模式(CRTP)应用--表达式模板(expression template) 2

    1 表达式模板 xff08 expression template xff09 概述 首先分几个部分介绍下expression template 1 1 表达式模板 xff08 expression template xff09 是什么 x