c++—可变参数、强枚举、多类型存储(variant)、动态类型(any)、和类型(optional)

1. 可变参数

    (1)C语言中的可变参数,其原理是利用所有的参数在找空间的保存位置是连续的;内部的程序主要是有va_list指针;

    (2)c++的可变参数,就是指initializer_list列表初始化,本质是一个表示某种特定类型的值的数组,可以理解为一个不可扩充的容器;

        ①初始化的区别:这就是为什么很多c++书籍喜欢用{}进型初始化的原因,因为检查严格;

int c = 3.3;  //符合,因为系统内会隐式转换,去尾,C语言风格
int b = {3.3};  //不符合,因为这里默认的是initializer_list<double>,无法实现double到int的转换;
int a {4.5};    //不符合,因为这里默认的是initializer_list<double>,无法实现double到int的转换;

        ②优点:有严格的类型检查,不会允许不同类型之间的隐式转换,这一点不同于C语言;

        ③用处:主要用在构造函数,把初始化列表当做参数,示例如下:

#include <iostream>

using namespace std;

class A
{
public:
    A(int num):m_num(num)  
    {
    }

    A(const initializer_list<int> v)  //这里是自定义的,与系统默认生成的带可变参数的构造函数相同,不同的是自定义的可以传多个参数,而系统默认的则只能根据属性(m_t的个数)传递;
    {
        for(auto temp : v)
        {
            cout<<temp<<endl;
        }
    }

    int m_num; 
};

void func1(initializer_list<int> t)    //传进来的是一个可变参数列表,需要通过迭代器遍历
{
    //cout<<t<<endl;
    auto it = t.begin();
    for(; it != t.end(); it++)
    {
        cout<<*it<<endl;
    }
}

int main(int argc, char **argv)
{
    initializer_list<int>init_list = {1,3,5,7,9};
    auto it = init_list.begin();    //可变参数列表不支持[],需要利用迭代器
    for(; it!= init_list.end(); it++)
    {
        cout<<*it<<endl;
    }

    func1({2,4,6,8,10});

    A a(1);
    A a1{200};  //此时,类A中默认生成含有初始化列表的默认构造函数;当没有自定义的initializer_list时,这里的200相当于给m_num赋值,当有时,则不是赋值;
    cout<<a1.m_num<<endl;
    A a2{3,5,7,9};
    cout<<a2.m_num<<endl;

    int count = {3};      //带{}的,系统内转化成了initializer_list<int>
    // int len = {3.14};  //initializer_list<double> v = 3.14,无法将double赋给int
    // int m_count {3.3};  //同样会报错,存在double到int的不合适转化,这里不同于C语言的隐式转换

    return 0;
}

2. 强枚举类型

    (1)和C语言的枚举类型相比,优势:

        ①强作用域:使用类型成员的时候必须前缀上所属的枚举类型的名字;

        ②可以执行底层类型:枚举类型后面使用了冒号以及类型关键字来执行枚举中的枚举值的类型

        ③转换限制(本质原因是强枚举是一种类型,成员尽管设置成int,但还是该枚举类型下的int,与外部的int不同):强类型枚举成员的值不可以与整型变量进行转换(隐式与显式都不可以)、不能将其与整数数字进行比较、不能对不同的枚举类型的枚举值进行比较、相同类型的枚举值可以比较;示例如下

扫描二维码关注公众号,回复: 16894633 查看本文章
#include <iostream>

using namespace std;

enum RET1{OK=1,NO=2};
enum class RET2:int{OKK=1,NOO=2};  //表示了指定底层类型的优点

RET2 test(int num)  //返回值的类型似乎enum枚举类型,这里不能写作int
{
    if(num == 5)
    {
        return RET2::OKK;
    }
    else
    {
        return RET2::NOO;
    }
}

int main(int argc, char **argv)
{
    RET2 n = test(7);  //注意枚举也是一种数据类型,这里不能用作int
    if(n == RET2::OKK) //表示了强作用域的优点,需要前缀枚举类型名称;相同的枚举类型之间可以比较;
    {
        cout<<"等于"<<endl;
    }
    else
    {
        cout<<"不等于"<<endl; 
    }

    // int n = RET2::OKK;  //不能利用强枚举类型的成员与整型进行转换,隐式与显式的都不可以

    return 0;
}

3. 多类型存储(variant)(一个类型安全的类)

    (1)variant 产生的原因是C语言中的union,其成员只能是同一种类型;而variant则优化了该问题, 是一个类型安全的联合体,本质是一个类模板;其特点是①当前值的类型总是已知的(有成员函数方法判断);②可以有任何指定类型的成员;③因为其是一个类,所以可以派生类;

    (2)注意事项

        ①variant不容许保有引用、数组,或类型void;

        ②variant容许保有同一类型多于一次,而且保有同一类型的不同cv限定版本;

        ③默认构造的variant保有其首个选项的值,除非该选型不是可默认构造的??;

    (3)当vatiant作为参数传递时,如何知道此时共用体varitant此时保存的是什么类型的成员?可以利用std::holds_allterative<int>(v),如果此时传来的共用体保存的是int类型的成员,那么该语句就是true,示例如下:

#include <iostream>
#include <variant>

using namespace std;

class A
{
public:
    A() = default;
    A(int len):m_len(len)
    {

    }
    friend ostream& operator<<(ostream &out, const A &a);
    int m_len;
};

ostream& operator<<(ostream & out, const A &a)
{
    out<<a.m_len;
    return out;
}

using namespace std;

using p_type = std::variant<int ,string, double>;

void print(p_type &v)
{
    if(std::holds_alternative<int>(v))  //利用std::holds_alternative<>判断传来的共用体里面是什么类型的值
    {
        // cout<<v.emplace()<<endl;
        cout<<v.index()<<" ";  //输出当前类型的索引号
        cout<<std::get<int>(v)<<endl;
    }
    if(std::holds_alternative<string>(v))
    {
        cout<<v.index()<<" ";
        cout<<std::get<string>(v)<<endl;
    }
    if(std::holds_alternative<double>(v))
    {
        cout<<v.index()<<" ";
        cout<<std::get<double>(v)<<endl;
    }
}

int main(int argc, char **argv)
{
    p_type v;
    v = 7;
    print(v);

    v = "hello";
    print(v);

    v = 3.14;
    print(v);

    //std::variant<A,int ,string, double> v2;  //无法引用 "std::variant<A, int, std::string, double>" 的默认构造函数 -- 它是已删除的函数C/C++(1790)
    //std::variant<std::monostate,A,int ,string, double> v2; //若第一个共用体成员是类型,则要求该类型需要有默认构造函数,因为类型A没有了默认构造函数,所以需要std::monostate占位符,同时不会影响共用体varitant的内部结构,单纯站位,避免报错
    std::variant<A,int ,string, double> v2;
    cout<<"-----"<<endl;
    v2.emplace<0>(A(7));
    cout<< get<A>(v2) <<endl;  //方式一:采用内部类型方式,且重载了<<运算符
    cout<< get<0>(v2).m_len <<endl;  //方式二:采用了索引方式,且在这里直接访问的类成员


    v2.emplace<2>("world");
    cout<<get<2>(v2)<<endl;

    return 0;
}

    (3)常用方法(成员函数与非成员函数)

        成员函数:

        ①index():输出当前共用体成员在variant共用体中的索引(0-n);

        ②emplace():原地构造函数,即相当于给variant里面的共用体某种成员赋值初始化;上图有例子;

        非成员函数:

        ①std::monostate,主要应用场景是,当varitant的第一个成员是类型时,比如A,当A没有默认无参构造函数时(默认无参构造函数没有指明default,另外有自定义有参构造函数,此时系统默认的无参构造函数将不会创建),就会报错,如上图;此时需要用std::monostate占位符,它会默认成为一个有默认构造函数的替代类型,从而满足varitant对于第一个成员类型的要求;

        ②std::holds_alternative,是用来检查当前共用体里面存储的是什么类型,通用用于varitant传参后,与if和else配合使用,针对不同的类型进行不同的操作;

        ③std::get,用以访问varitant内部的成员,有两种方式,一种是索引,一种是类型名,如上图所示;

        ④std::visit,主要应用于方位varitant变量,根据它的当前类型做不同的操作,像上图是利用的holds_alternative和if_else配合进行判断的,稍显复杂;而std::visit则是一个专门配合varitant的访问器,可以以函数式的方法访问std::variant变量;

        std::visit的参数列表是不定长的,可以传入多个varitant变量;使用示例如下:

简单用法,配合函数对象:

#include <iostream>
#include <variant>

using namespace std;

struct PrintVisitor  //将函数封装成类,利用函数对象进行处理
{
    void operator()(int i)  //重载了函数运算符,然后是分别是三种类型的函数重载,(int i)(double d)(string s),分别对应varitant中的三种类型
    {
        cout<<"int: "<<i<<endl;
    }

    void operator()(double d)
    {
        cout<<"double: "<<d<<endl;
    }

    void operator()(const std::string &s)
    {
        cout<<"str: "<<s<<endl;
    }
};

int main(int argc, char** argv)
{
    std::variant<int,double,string>value = 1.0;
    std::visit(PrintVisitor(),value);

    return 0;
}

        复杂用法,配合lanbda表达式:(求圆形、长方形的面积和周长)

// 值多态,适用于新增方法,已经定义好了类,要在里面增加计算周长,原先是计算面积;
#include <variant>
#include <iostream>
using namespace std;

constexpr double pi = 3.14;
struct Shape2  //抽象类
{
   virtual void getName() = 0;
};

struct Circle :  public Shape2
{
   Circle()
   {
   }

   Circle(double r) : r(r)
   {
   }

   void getName()
   {
       cout << "Circle" << endl;
   }
    double r;
};
double getArea(const Circle& c) 
{
    return pi * c.r * c.r;
}
double getPerimeter(const Circle& c) 
{
    return 2 * pi * c.r;
}

struct Rectangle :public Shape2
{
public:
    Rectangle(double w, double h) : w(w), h(h)
    {
    }

    void getName()
    {
        cout << "Rectangle" << endl;
    }
    double w;
    double h;
};
double getArea(const Rectangle& c) 
{
    return c.h * c.w;
}
double getPerimeter(const Rectangle& c) 
{
    return 2 * (c.h + c.w);
}

using Shape = std::variant<Circle, Rectangle>;  //重命名,variant可存储的类型这里是这3种
double getArea(const Shape& v) 
{
    return std::visit([](const auto & data) {return getArea(data); }, v);
}
double getPerimeter(const Shape& v) 
{
    return std::visit([](const auto& data) {return getPerimeter(data); }, v);
}

int main() 
{
    Shape sp = Circle(2);
    std::cout<<getArea(sp) << endl;
    std::cout << getPerimeter(sp) << endl;;

    sp = Rectangle(2,3);
    std::cout << getArea(sp) << endl;
    std::cout << getPerimeter(sp) << endl;
}

        ⑤两种多态的比较

        第一种:类型多态(subtype):由继承和虚函数实现,即用基类的指针或引用调用派生类的指针或引用,就可以调用派生类中的方法,实现多态;

        特点:(适合扩展类,不适合扩展方法),适用于新增类,不适合新增方法(因为新增方法的话,在基类和派生类都要增加,违反了扩展性原则),举例,现有基类是计算各图形的面积,两个这样的虚函数,目前已经有圆形的矩形两个类,这是类型多态适合新增一个三角形类,不适合新增计算周长的方法,因为这样基类和派生类都要新增这个方法,而新增三角形类的话,只需要新增后继承基类即可;

        第二种:值多态,是基于函数重载与varitant配合实现的;如上例中的《visit简单用法,配合函数对象》,就是利用函数的参数类型的不同实现的多态;

        特点:(适合扩展方法,不适合扩展类),为所有类新增一个方法较容易,例如在《visit简单用法,配合函数对象》中,就可以在varitant共用体中新增一个类,然后在函数对象struct printvisitor中新增对应的函数就可以了,注意修改参数类型,适配新增的类即可;

4. 动态类型std::any(可以作为函数形参、返回值)

    (1)std::any是代替c的void*,单数any不是类模板,是一种值的类型,使用与保存任何类型的值;

        优点:可以作为形参,接收任何类型的值;

        缺点:频繁的获取空间、释放空间;降低了程序的运行效率;且当传入的数据大于32字节时,any会创建堆内存,额外new一次存储空间,将之前保存的数据复制过来,但是之前的空间并不释放,将导致内存泄漏问题;

    (2)定义与访问内部值:要访问包含的值,需要使用std::any<类型>(变量名),来访问该变量内部的值;示例如下:

#include <iostream>
#include <any>

using namespace std;

int main(int argc, char **argv)
{
    std::any n1 = 7;
    cout<< std::any_cast<int>(n1) <<endl;  //使用std::any_cast<类型>(变量名) 提取内部值

    std::any n2 = string("hello,world");   //注意,需要加string进行转化,因为单独的“hello world”是const char *
    cout<< std::any_cast<std::string>(n2) <<endl;

    return 0;
}

    (3)可以作为函数形参、返回值,且可以利用type()和typeid()判断当前any存储的类型,示例如下:

#include <iostream>
#include <any>

using namespace std;

std::any test(std::any &n)
{
    if(n.type() == typeid(int))  //判断any当前的类型值
    {
        cout<< std::any_cast<int>(n) <<endl;
    }
    if(n.type() == typeid(std::string))
    {
        cout<< std::any_cast<std::string>(n) <<endl;
    }

    std::any ret = (std::string)"well done!";
    return ret;
}

int main(int argc, char **argv)
{
    std::any n1 = 7;
    test(n1);

    std::any n2 = string("hello,world");   //注意,需要加string进行转化,因为单独的“hello world”是const char *
    cout<< std::any_cast<std::string>( test(n2) ) <<endl;  //打印test(n2)的返回值

    return 0;
}

5. 和类型std::optional

    (1)如何处理无参返回或者返回多个返回值的问题?

        方法一:bool func1(int  param1,  int * param2);这种方式浪费存储空间,无论是否有返回值,都需要按照有返回值做准备;

        方法二:采用pair对组和tuple元组;

        pair对组:

        ①pair称为对组,可以将两个值视为一个单元;

        ②pair<T1,T2>存放的这两个值的类型不一样,如T1是int,T2是string,也可以是自定义类型;

       ③pair.first代表pair里面的第一个值,pair.second代表pair里面的第二个值;

        示例如下

#include <iostream>
#include <optional>

using namespace std;

struct Out
{
    string out1{""};
    string out2{""};
};

pair<bool,Out>func(const string & in)
{
    Out o;
    if(in.size() == 0)
    {
        return {false,o};
    }
    o.out1 = "hello";
    o.out2 = "world";
    return {true,o};
}

int main(int argc, char **argv)
{
    if(auto[status,o] = func("hi");status)  //前面是对对组初始化,后面的status是判断false还是true?
    {                                       //对组的初始化使用[],如上面的auto
        cout<<status<<endl;  //status为true;
        cout<<o.out1<<endl;
        cout<<o.out2<<endl;
    }

    pair<int, double>p1;   //定义p1
    p1 = make_pair(3,6.7); //初始化p1

    cout<<p1.first<<endl;  //输出第一个成员
    cout<<p1.second<<endl; //输出第二个成员

    return 0;
}

        tuple元组

        ①tuple容器(元组)是一种容器,是不包含任何结构的、快速而低质的,用于函数返回多个值,在主函数利用auto [ ]接收对应的元组返回值即可,注意[ ]中的类型与函数返回值的类型保持一致;

        ②两种初始化方式、一种获取元素个数方式、两种访问元素方式、一种获取元素类型方式,示例如下:

#include <iostream>
#include <tuple>

using namespace std;

tuple<bool,string,string> func(int num)  //返回值是元组类型
{
    if(num != 7)
    {
        return make_tuple(false,"the num ","not equal 7");  
    }
    else
    {
        return make_tuple(true,"the num ","equal 7");
    }
}

int main(int argc, char **argv)
{
    if(auto[status,out1,out2] = func(7);  status)  //if在c++20中的新功能,可以这里初始化变量,这里是元素变量
    {
        cout<<out1<<out2<<endl;
    }

    //两种初始化的方式:
    auto t1 = make_tuple<int,string,double>(7,std::string("name"),5.66); //方式一:利用make_tuple

    tuple<int,string,double> t2(9,"tel",7.98); //方式二:直接初始化
    
    //一种获取元素个数的方式:
    int n = std::tuple_size<decltype(t2)>::value;  //获取元组t2内部的元素个数,注意模板里面替换t2即可,dectype和value都是模板,不用改变
    cout<<"t2 has "<<n<<" elements"<<endl;

    //两种获取元组内部值的方式:
    cout<<"the elements is ";  //方式一:通过索引
    cout<<get<0>(t2)<<" ";
    cout<<get<1>(t2)<<" ";
    cout<<get<2>(t2)<<endl;

    int num; 
    string s1;
    double dou;
    std::tie(num,s1,dou) = t2; //方式二:通过tie解包,需要提前定义所有符合元组类型的单独的变量,不可或缺
    cout<<"the elements is "<< num << " " << s1 <<" "<< dou <<endl;

    //一种获取元素类型的方式:
    std::tuple_element<2,decltype(t2)>::type temp;  //获取索引2的类型,此时temp为double类型的变量
    temp = std::get<2>(t2);  //获取实际的元素值
    cout<<"temp = "<<temp<<endl;

    return 0;
}

运行结果

        方法三:采用std::optional,其管理的是可选的容纳值;即如果函数成功执行,则返回含实例的值,如果执行失败,则返回不含实例的值;下面详述std::optional;

6. std::optional

        概述:其实optional就是一个类,只不过这个类的成员可以存在,也可以不存在;比如,当你设置optional的成员是一个tuple类时,作为返回值,那么符合条件是你可以返回有你想要的信息在里面的tuple类,此时是栈空间的;而不符合条件时,则返回nullopt,此时不占用空间;在主函数接收返回值时,可以利用auto推导类型,但是需要注意的是此时的返回值还是optional的类型,需要利用其成员函数value()取其值(可以先接受在value,也可以先对返回值value再输出),此时就得到成员tuple,然后在利用tie解包就可以看到里面的信息;同样的,成员也可以是一个自定义类,但是要注意重载<<运算符;详见下例

        ①定义std::optional<T>,是一个满足可析构的类型;

        ②初始化:使用=用另一个T类型含值std::optional初始化、使用构造函数初始化(以nullopt_t类型值或是T类型值)、默认构造函数;

        ③判断是否含值的方式:使用bool hasValue = temp.has_value()检查是否含值;

        ④取值的方式:使用(*temp)取T,即默认为T的指针,使用temp.value()获取T值,使用temp.value_or()获取值(存在值)或者其他(不存在值);

        ⑤返回:返回一个T对象(非指针)、nulloptl;

示例如下:

#include <iostream>
#include <tuple>
#include <string>
#include <optional>
#include <set>

using namespace std;

using tu = tuple<int,string>;

optional<tu> func1(int num)  //返回值是optional类型(成员是tuple<int,string>)
{
    if(num == 5)
    {

        return std::make_tuple(100,"ok");
    }
    else
    {
        return nullopt;
    }
}

class A
{
public:
    A() = default;
    A(int t):m_t(t)
    {

    }
    friend ostream& operator<<(ostream&out, A &a);
    int m_t;
};
ostream& operator<<(ostream&out, A &a)
{
    out<<a.m_t;
    return out;
}

optional<A>func2(int num)  //返回值是optional类型(A)
{
    if(num = 7)
    {
        return A(567);
    }
    else
    {
        return nullopt;
    }
}

int main(int argc, char **argv)
{
    int id = 0;
    string msg{};

    auto t4 = func1(5);  //方式一:先接收返回值,然后对返回值取值(value)
    cout<<t4.has_value()<<endl;  //判断返回值t4是否含有值,有是1,无是0
    std::tie(id,msg) = t4.value();  //因为这里的返回值是optional,而tuple是它的一个成员,所以这里需要调用optional的成员函数value(),将内部成员取出,因为该成员是tuple,所以利用tie解包
    cout<< id <<" "<< msg <<endl;
                                
    auto t5 = func2(7).value();  //方式二:直接对返回值取值(value),然后就可以直接操作成员了;需要利用optional的成员函数value取其值,之后就是一个A类型变量
    cout<<t5<<endl;  

    return 0;
}

猜你喜欢

转载自blog.csdn.net/m0_72814368/article/details/130956394