`
qqsunkist
  • 浏览: 32853 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

探究boost::bind的实现

    博客分类:
  • C++
阅读更多

泛化的bind实用性很强,支持将泛化函数(函数、函数指针、仿函数、与成员函数)与运行期实参的动态绑定;在实际工程中也经常作为基础工具集之一被频繁地使用;我计划投入一定的精力,仔细阅读boost库中的基础工具集的代码实现(bind是第一个);旨在了解这些工具的内部实现机理同时也提高下自己的C++语言泛型编程技术,以备后续的实际工程中使用;

 

2005年,Andrei Alexandrescu就在他的C++巨著《C++设计新思维中》中的5.10节中提出了泛型化的函数绑定;bind的实现目标非常明确,就是在运行期实现针对泛化函数任意类型、任意个数的实参进行绑定;形式如下:

bind(f, _2, _1)(x, y);                  // f(y, x)

bind(g, _1, 9, _1)(x);                 // g(x, 9, x)

bind(g, _3, _3, _3)(x, y, z);        // g(z, z, z)

bind(g, _1, _1, _1)(x, y, z);        // g(x, x, x)

根据上述的形式,假如对泛化函数不考虑的话,至少涉及任意个参数(理论上),任意类型、任意位置的实参与函数绑定;另外,涉及占位符等,值拷贝、引用或者移动参数传递、返回值等细节技术问题。

 

我们一步一步先进行拆解;从上述调用的形式分析,bind函数是function template,其最重要的作用是(个人认为)是进行参数类型推导;function template会根据不同的参数个数,而被重载;实现代码中支持最多9个参数;代码如下:(此处列出的是两个参数的函数模板)

 

template<class R, class F, class A1, class A2>
    _bi::bind_t<R, F, typename _bi::list_av_2<A1, A2>::type>
    BOOST_BIND(F f, A1 a1, A2 a2)
{
    typedef typename _bi::list_av_2<A1, A2>::type list_type;
    return _bi::bind_t<R, F, list_type> (f, list_type(a1, a2));
}
       1、 根据函数重载规则(9个实参范围内, A表示参数,英语Argument,A1, A2, …… A9分表表示9种实参类型,当然9个实参类型可重复,可相同)选择对应参数个数的bind函数模板会别Instantiation。此过程根据函数模板的特性,自然地推导出参数A1,A2;

       2、被推导出的参数类型A1, A2,通过class template _bi::list_av_2 组成了一个由两个类型元素合成的类型列表;新组成的类型列表被typedef成list_type类型

       3、list_type(a1, a2) 被显式地的调用构造函数;

       4、bind的返回类型为 _bi::bind_t<R, F, list_type>; 且实际真正实现是通过调用class template _bi::bind_t的函数调用(operator () .... )的形式实现的。bind_t是被重载了的仿函数!

       5、值得注意的是:F类型可以通过bind的调用,被推导出来;但是返回类型R, 目前还是推导不出来哦!

 

       接下来,我们先看class template _bi::list_av_2的实现代码;

       

template<class T> class value
{
public:
    value(T const & t): t_(t) {}

    T & get() { return t_; }
    T const & get() const { return t_; }

    bool operator==(value const & rhs) const
    {
        return t_ == rhs.t_;
    }

private:
    T t_;
};
namespace _bi
{
// add_value
// 参数类型是占位符时,add_value<T>::type是boost::arg<I>;与类型T无关了
template< class T, int I > struct add_value_2 {
    typedef boost::arg<I> type;
};
//参数类型非占位符时,add_value<T>::type实际就是_bi::value<T>
template< class T > struct add_value_2< T, 0 >{
    typedef _bi::value< T > type;
};
template<class T> struct add_value{
    typedef typename add_value_2< T, boost::is_placeholder< T >::value >::type type;
};
template<class T> struct add_value< value<T> >{
    typedef _bi::value<T> type;
};
template<class T> struct add_value< reference_wrapper<T> >{
    typedef reference_wrapper<T> type;
};
template<int I> struct add_value< arg<I> >{
    typedef boost::arg<I> type;
};
template<int I> struct add_value< arg<I> (*) () >{
    typedef boost::arg<I> (*type) ();
};
template<class R, class F, class L> struct add_value< bind_t<R, F, L> >{
    typedef bind_t<R, F, L> type;
};

} 
template<class A1, class A2> struct list_av_2
{
    typedef typename add_value<A1>::type B1;
    typedef typename add_value<A2>::type B2;
    typedef list2<B1, B2> type;
};

    这段代码中,主要就是理解add_value类模板针对各种情况的特化;单独看代码貌似比较难理解;以例子代入的话,会容易些;调用实例如下述代码:

 

#include <iostream>

struct sum {
      typedef double result_type;
      double operator()(int a, double b) {
             return a+b;
      }
};
int main(int argc, char *argv[])
{
      // 情况1
      std::cout << boost::bind(sum(), _1, _2)(3, 4.0) << std::endl;
      // 情况2
      std::cout << boost::bind(sum(), 3, 4.0)() << std::endl;
      // 情况3
      std::cout << boost::bind(sum(), _1, 4.0)(3) << std::endl;
      return 0;
}

    情况1:A1: boost::arg<1>, A2: boost::arg<2>; B1: boost::arg<1>, B2: boost::arg<2>; 

                 list_type = list2<B1, B2>

    情况2:A1: int, A2: double; B1: value<int>, B2: value<double>;

                 list_type = list2<B1, B2>

    情况3:A1: boost::arg<1>, A2: double; B1: boost::arg<1>, B2: value<double>; 

                 list_type = list2<B1, B2>

    根据argument deduction的结果进行分析,本次进行参数推导的参数列表是第一对圆括号内的。第二个实参列表并木有丢失!我们把第一对圆括号内的参数,简称第一参数列表;第二对圆括号内的参数,简称第二参数列表;注意,第二参数列表才是真正的实际参数;第一参数列表里可能包含占位符参数;

template< class A1, class A2 > class list2: private storage2< A1, A2 >
{
private:
    typedef storage2< A1, A2 > base_type;
public:
    list2( A1 a1, A2 a2 ): base_type( a1, a2 ) {}

    A1 operator[] (boost::arg<1>) const { return base_type::a1_; }
    A2 operator[] (boost::arg<2>) const { return base_type::a2_; }

    A1 operator[] (boost::arg<1> (*) ()) const { return base_type::a1_; }
    A2 operator[] (boost::arg<2> (*) ()) const { return base_type::a2_; }

    template<class T> T & operator[] (_bi::value<T> & v) const { return v.get(); }
    template<class T> T const & operator[] (_bi::value<T> const & v) const { 
         return v.get(); 
    }

    template<class T> T & operator[] (reference_wrapper<T> const & v) const { 
         return v.get(); 
    }

    template<class R, class F, class L> 
    typename result_traits<R, F>::type operator[] (bind_t<R, F, L> & b) const { 
        return b.eval(*this); 
    }

    template<class R, class F, class L> 
    typename result_traits<R, F>::type operator[] (bind_t<R, F, L> const & b) const { 
        return b.eval(*this); 
    }

    template<class R, class F, class A> R operator()(type<R>, F & f, A & a, long){
        return unwrapper<F>::unwrap(f, 0)(a[base_type::a1_], a[base_type::a2_]);
    }

    template<class R, class F, class A> 
    R operator()(type<R>, F const & f, A & a, long) const {
        return unwrapper<F const>::unwrap(f, 0)(a[base_type::a1_], a[base_type::a2_]);
    }

    template<class F, class A> 
    void operator()(type<void>, F & f, A & a, int){
        unwrapper<F>::unwrap(f, 0)(a[base_type::a1_], a[base_type::a2_]);
    }

    template<class F, class A> 
    void operator()(type<void>, F const & f, A & a, int) const{
        unwrapper<F const>::unwrap(f, 0)(a[base_type::a1_], a[base_type::a2_]);
    }

    template<class A> bool operator()( type<bool>, logical_and & /*f*/, A & a, int ){
        return a[ base_type::a1_ ] && a[ base_type::a2_ ];
    }

    template<class A> 
    bool operator()( type<bool>, logical_and const & /*f*/, A & a, int ) const{
        return a[ base_type::a1_ ] && a[ base_type::a2_ ];
    }

    template<class A> bool operator()( type<bool>, logical_or & /*f*/, A & a, int ){
        return a[ base_type::a1_ ] || a[ base_type::a2_ ];
    }

    template<class A> 
    bool operator()( type<bool>, logical_or const & /*f*/, A & a, int ) const{
        return a[ base_type::a1_ ] || a[ base_type::a2_ ];
    }

    template<class V> void accept(V & v) const{
        base_type::accept(v);
    }

    bool operator==(list2 const & rhs) const{
        return ref_compare(base_type::a1_, rhs.a1_, 0) && 
               ref_compare(base_type::a2_, rhs.a2_, 0);
    }
};

    list0, list1, list2, ... ... list9 是不同模板参数个数的listN 类模板实现;上述同样以list2为例说明list的实现;这个listN的类模板实现很重要,因为在接下来的真正被绑定的函数调用体被调用时_bi::bind_t<>::operator()(...),最终还是依赖于这个listN的实现;

    listN因为负责最终存储传入的第二参数列表中的实参,就必然有随机读取任意参数的功能;除此之外,listN还负责最终的绑定函数体的调用;因此listN的实现,主要有三部分功能:

    (1)存储最终调用函数体需要传入的参数;即,存储第二参数列表;

    (2)operator[] 的运算符的重载;【类似随机迭代器,对保存的实参列表进行随机访问】

    (3)operator() 运算符的重载;   【实现最终被绑定的泛化函数体的函数调用】

      功能(1)是listN通过storageN的私有继承得以实现;此处的继承使用了私有继承,私有继承的实现语义是”用……来实现“(“is implemented in terms of ");且子类在其成员函数里调用基类公有成员时,使用base::member_语法; 按照上述调用代码逻辑情况1为例,传入参数a1,a2分别是 _1, _2,被存储在storageN里。

 

namespace boost
{
namespace _bi
{
// 1

template<class A1> struct storage1
{
    explicit storage1( A1 a1 ): a1_( a1 ) {}

    template<class V> void accept(V & v) const
    {
        BOOST_BIND_VISIT_EACH(v, a1_, 0);
    }

    A1 a1_;
};

#if !defined( BOOST_NO_TEMPLATE_PARTIAL_SPECIALIZATION ) && !defined( __BORLANDC__ )

template<int I> struct storage1< boost::arg<I> >
{
    explicit storage1( boost::arg<I> ) {}

    template<class V> void accept(V &) const { }

    static boost::arg<I> a1_() { return boost::arg<I>(); }
};

template<int I> struct storage1< boost::arg<I> (*) () >
{
    explicit storage1( boost::arg<I> (*) () ) {}

    template<class V> void accept(V &) const { }

    static boost::arg<I> a1_() { return boost::arg<I>(); }
};

#endif

// 2

template<class A1, class A2> struct storage2: public storage1<A1>
{
    typedef storage1<A1> inherited;

    storage2( A1 a1, A2 a2 ): storage1<A1>( a1 ), a2_( a2 ) {}

    template<class V> void accept(V & v) const
    {
        inherited::accept(v);
        BOOST_BIND_VISIT_EACH(v, a2_, 0);
    }

    A2 a2_;
};

#if !defined( BOOST_NO_TEMPLATE_PARTIAL_SPECIALIZATION )

template<class A1, int I> struct storage2< A1, boost::arg<I> >: public storage1<A1>
{
    typedef storage1<A1> inherited;

    storage2( A1 a1, boost::arg<I> ): storage1<A1>( a1 ) {}

    template<class V> void accept(V & v) const
    {
        inherited::accept(v);
    }

    static boost::arg<I> a2_() { return boost::arg<I>(); }
};

template<class A1, int I> struct storage2< A1, boost::arg<I> (*) () >: public storage1<A1>
{
    typedef storage1<A1> inherited;

    storage2( A1 a1, boost::arg<I> (*) () ): storage1<A1>( a1 ) {}

    template<class V> void accept(V & v) const
    {
        inherited::accept(v);
    }

    static boost::arg<I> a2_() { return boost::arg<I>(); }
};

#endif

 

   上述截取了部分storageN的类模板代码,可以看出storageN是个单继承体系,N表示了继承体系的总层级,例如storage2则表示整体继承体系一共2层,第一层存储了参数a1, 第二层公有继承于storage1,存储参数a2;至于为什么使用这样的实现方式进行存储,后续再深入研究;另外需要注意的是,是A1,A2的类型决定了参数存储方式;如果const reference&, 实参是以引用的形式存储的。后续值得关注下,A1,A2在经过函数模板的类型推导后,是否存有中间加工的元编程处理过程;

   功能(3)的实现中使用了unwrapper类模板对F的外覆类进行解外覆处理;ref.hpp实际是boost中的另外一个基础核心工具集之一;后续单独研究下;

   接下来,就是重点分析下,如何从占位符到实参的绑定,并且bind_t的对绑定函数的实际调用;这里自然要先粘贴下_bi::bind_t代码的实现摘要;

   

template< class A > struct list_add_cref {
    typedef A const & type;
};

template< class A > struct list_add_cref< A& >{
    typedef A & type;
};

template<class R, class F, class L> class bind_t {
private:

    F f_;
    L l_;

public:

    typedef typename result_traits<R, F>::type result_type;
    typedef bind_t this_type;

    bind_t( F f, L const & l ): f_( f ), l_( l ) {}
    ... ... 
    template<class A1, class A2> result_type operator()( A1 && a1, A2 && a2 )
    {
        list2< typename list_add_cref<A1>::type, typename list_add_cref<A2>::type > a( a1, a2 );
        return l_( type<result_type>(), f_, a, 0 );
    }

    template<class A1, class A2> result_type operator()( A1 && a1, A2 && a2 ) const
    {
        list2< typename list_add_cref<A1>::type, typename list_add_cref<A2>::type > a( a1, a2 );
        return l_( type<result_type>(), f_, a, 0 );
    }
    …… …… 
    
     template<class A1, class A2> result_type operator()(A1 & a1, A2 & a2)
    {
        list2<A1 &, A2 &> a(a1, a2);
        BOOST_BIND_RETURN l_(type<result_type>(), f_, a, 0);
    }

     template<class A1, class A2> result_type operator()(A1 & a1, A2 & a2) const
    {
        list2<A1 &, A2 &> a(a1, a2);
        BOOST_BIND_RETURN l_(type<result_type>(), f_, a, 0);
    }
…… ……

    上述第一参数列表推导后,加工处理后的组成的list_type类型,被推导成L类型;同时,根据第二参数列表的个数及类型,会找对应参数个数的operator()实例化调用;第二参数列表会被推到成类型A1,A2 ....并组成另外一个listN类型且该类型的实例是a;如前所述,在组成listN的过程中,通过list_add_cref<T>元编程处理过程针对A1,A2推导的类型进行add const reference处理,以确保实参以最低成本存储在storageN中。L类型中的多个operator()的重载形式之一被调用时,list_type的operator[]实现了最终的占位符到实参(第二参数列表中的)的绑定;

分享到:
评论

相关推荐

    VB+ACCESS网吧计费系统(源代码+系统).zip

    1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。

    基于C#的小区物业管理系统.docx

    设计一个基于C#的小区物业管理系统是一个涉及多个方面和模块的复杂项目。这样的系统通常包括用户管理、房屋管理、费用管理、报修管理、投诉管理、公告管理等功能。下面我将简要概述如何构建一个基本的小区物业管理系统,包括其主要模块和一些关键概念。 1. 系统架构设计 前端界面:可以使用WinForms或WPF来构建桌面应用程序界面,也可以使用ASP.NET Core MVC或Blazor来构建Web界面。 后端逻辑:使用C#作为后端开发语言,结合.NET Core或.NET Framework来编写业务逻辑。 数据库:可以使用SQL Server、MySQL或SQLite等数据库来存储系统数据。 架构模式:考虑使用MVC(模型-视图-控制器)或三层架构(表示层、业务逻辑层、数据访问层)来组织代码。 2. 主要功能模块 2.1 用户管理 功能:管理小区业主、租户、物业工作人员等用户的信息。 关键操作:用户注册、登录、密码找回、用户信息编辑、权限管理等。 2.2 房屋管理 功能:管理小区内所有房屋的信息,包括房屋位置、面积、户型、业主信息等。 关键操作:房屋信息录入、查询、修改、删除、房屋状态更新

    4-3_Education_BLUE_2017_03-CL-20180524MTAX.potx

    微软演示材料

    C#通过COM读取Excel到dataGridView

    C#通过COM读取Excel到dataGridView

    4-3_Consumer_BLUE_2017_03.potx

    微软演示材料

    4-3_Business_DK_BLUE_2017_03-CL-20180524MTAX.potx

    微软演示材料

    【精品毕设推荐】-基于微信小程序的儿童预防接种预约系统设计与实现.zip

    【项目简介】 可辅助在本地配置运行 随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了儿童预防接种预约微信小程序的开发全过程。通过分析医院挂号信息管理的不足,创建了一个计算机管理医院挂号信息的方案。文章介绍了儿童预防接种预约微信小程序的系统分析部分,包括可行性分析等,系统设计部分主要介绍了系统功能设计和数据库设计。 本儿童预防接种预约微信小程序可以实现管理员和用户。管理员功能有个人中心,用户管理,儿童信息管理,疫苗信息管理,儿童接种管理,儿童接种史管理,医疗机构管理,预约接种管理,系统管理等。用户功能有注册登录,儿童信息,疫苗信息,儿童接种,儿童接种史,医疗机构,预约接种,我的收藏管理等。因而具有一定的实用性。 本站后台采用Java的SSM框架进行后台管理开发,可以在浏览器上登录进行后台数据方面的管理,MySQL作为本地数据库,微信小程序用到了微信开发者工具,充分保证系统的稳定性。系统具有界面清晰、操作简单,功能齐全的特点,使得医院挂号信息管理工作系统化、规范化。 关键词:儿童预防接种预约微信小程序;SSM框架;MYSQL

    【路径规划】用于路径规划的星形算法Matlab实现.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    java基于ssm+jsp 农场信息化管理系统源码 带毕业论文+ppt+sql

    1、开发环境:SSM框架;内含Mysql数据库;jsp技术;内含说明文档 2、项目代码都经过严格调试,代码没有任何bug! 3、该资源包括项目的全部源码,下载可以直接使用! 4、本项目适合作为计算机、数学、电子信息等专业的课程设计、期末大作业和毕设项目,作为参考资料学习借鉴。 5、本资源作为“参考资料”如果需要实现其他功能,需要能看懂代码,并且热爱钻研,自行调试。

    node-exporter安装所需要镜像

    node-exporter安装所需要镜像

    【高创新】基于人工鱼群算法ASFO-Transformer-BiLSTM实现故障识别Matlab实现.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。 替换数据可以直接使用,注释清楚,适合新手

    Java期末速成最终版.pdf

    一、Java语言基础知识,包括面向对象编程、语法特性等 0、什么是面向对象编程? 1、基本语法: 2、变量和数据类型: 3、运算符和表达式: 4、控制结构: 5、函数和方法: 6、类和对象: 7、继承和接口: 8、异常处理: 二、Java核心类库和异常处理机制 1、Java核心类库 2、Java异常处理机制

    食品2022醉鹅娘复盘方案(品牌传播、社群营销)

    【食品】2022醉鹅娘复盘方案(品牌传播、社群营销)

    gitlab搭建与日常使用

    1.Git是一种版本控制系统,是一种工具,用于代码的存储和版本控制。 2.GitHub是一个基于Git实现的在线代码仓库,是目前全球最大的代码托管平台,可以帮助程序员之间互相交流和学习。 3.GitLab是一个基于Git实现的在线代码仓库软件,你可以用GitLab自己搭建一个类似于GitHub一样的仓库, 但是GitLab有完善的管理界面和权限控制,一般用于在企业、学校等内部网络搭建Git私服。 4.GitHub和GiLlab两个都是基于Web的Git远程仓库,它们都提供了分享开源项目的平台, 为开发团队提供了存储、分享、发布和合作开发项目的中心化云存储的场所。从代码的私有性上来看,GitLab 是一个更好的选择。 但是对于开源项目而言,GitHub依然是代码托管的首选。

    CSDN_1726656341213.png

    CSDN_1726656341213.png

    动画/卡通着色器:RealToon Pro AnimeToon Shader v5.0.8p10 (07 Aug 2024)

    AAA 级动画/卡通着色器,让你的角色和对象看起来像逼真的动画/卡通。 动画/卡通渲染着色器。 (适用于游戏、电影/动画和插图/美术) - 第一款完全支持 Unity HDRP 和 DXR/光线追踪的动画/卡通着色器。 - 支持 Unity 版本 5、2017、2018、2019、2020 to 2022 ,以及将来的 Unity 版本。 - Supports Unity 6 and beyond. - (PC、Mac 和 Linux)、移动端和游戏主机(Nintendo Switch 和 Xbox)。 - PlayStation (RealToon URP and RealToon HDRP). - 完整的多光照。 - 可与实时阴影一起使用,或单独使用。 功能: - 平滑对象法线: *平滑的对象法线,以便更整洁地着色。 - 自阴影: *(无需使用纹理图即可调整尺寸/阈值和硬度。) - 法线贴图: *(为了更多细节或覆盖对象法线。)

    如果你的项目需要使用 C++ 来训练大规模模型,这些框架提供了便利的接口和功能,同时能够处理复杂的训练任务

    在 C++ 中进行大模型训练通常需要借助现有的深度学习框架如 PyTorch C++ 前端 或 TensorFlow C++ API。这些框架不仅支持高效的 C++ 编程,还能充分利用硬件资源进行加速。如果你的项目需要使用 C++ 来训练大规模模型,这些框架提供了便利的接口和功能,同时能够处理复杂的训练任务。

    【高创新】基于豪猪优化算法CPO-Transformer-BiLSTM实现故障识别Matlab实现.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。 替换数据可以直接使用,注释清楚,适合新手

    4-3_Business_ORANGE_2017_03.potx

    微软演示材料

    java-ssm+vue共享充电宝管理系统实现源码(项目源码-说明文档)

    管理员登录,通过填写用户名、密码、角色等信息,输入完成后选择登录即可进入共享充电宝管理系统 管理员登录进入共享充电宝管理系统可以查看首页、个人中心、用户管理、维护人员管理、区域信息管理、合作商户管理、充电宝投放管理、租赁订单管理、归还订单管理、费用订单管理、充电宝维护管理、公告栏管理、系统管理等内容 项目关键技术 开发工具:IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7+ 后端技术:ssm 前端技术:Vue 关键技术:springboot、SSM、vue、MYSQL、MAVEN 数据库工具:Navicat、SQLyog

Global site tag (gtag.js) - Google Analytics