IE盒子

搜索
查看: 97|回复: 6

现代C++学习——实现动态类型std::any

[复制链接]

3

主题

7

帖子

11

积分

新手上路

Rank: 1

积分
11
发表于 2023-1-12 12:43:29 | 显示全部楼层 |阅读模式
在诸如python,js等脚本语言中,其变量都是动态类型的。
a = 3
a = 3.14
a = "hello wrold"在C/C++中,如何实现一个类似的功能呢?
Any x = 3;
cout << x << endl;
x = 3.14;
cout << x << endl;
x = string("hello wrold");
cout << x << endl;
首先,在C语言中,很多库都是使用 void * 来实现动态类型的。
因为void * 可以指向任意类型。
struct A{

};
struct B {

};
int main() {
    void* p = new A();
    p = new B();
    p = new vector<int>();
}
同样的,还有父类型的指针可以指向子类型
struct A{

};
struct B  : A{

};
struct C : A {

};
int main() {
    A* p = new B();
    p = new C();
}
当然在一些限定了类型种类的语言解析器中。会使用union加一个type 来实现动态类型。


有关union 的文章 严格鸽:C/C++ union 使用教程 (常见操作与缺陷)
这样的做法实际上更类似std::variant
这里,我们准备利用父类指针可以指向子类来实现一个std::any
我们一步步的来,首先是父类与子类
struct Base {

};
template<typename T>
struct Data : Base{
    T data;
    Data(const T& t) : data(t) {}
};
int main() {
    Base* p;
    p = new Data<string>("hello world");
    p = new Data<int>(3);  
}
通过模板,生成不同的Data子类。
不过每次都要写Data<type>不是太美观,所以用一个类包一下吧
struct Any {
    template<typename T>
    Any(const T& t) : ptr{ new Data<T>(t) } {};
    struct Base {

    };
    template<typename T>
    struct Data : Base {
        T data;
        Data(const T& t) : data(t) {}
    };
    Base *ptr;
};
struct A{
    int x, y;
};
int main() {
    Any x = 2;
    x = 3.141;
    x = A{ 3,5 };
}
emmmm,感觉不是,但是是否发现了一个问题。
我们访问不了数据啊。。。
毕竟这个Any里面只有一个Base的指针,而真正的内容在子类里面。所以呢?我们需要加上一个强制类型转换来获得子类的数据。
struct Any {
    template<typename T>
    Any(const T& t) : ptr{ new Data<T>(t) } {};
    struct Base {

    };
    template<typename T>
    struct Data : Base {
        T data;
        Data(const T& t) : data(t) {}
    };
    Base *ptr;
    template<typename T>
    T& get_data() {
        return ((Data<T>*)ptr)->data;
    }
};
struct A{
    int x, y;
};
int main() {
    Any x = 2;
    x = 3.141;
    cout << x.get_data<double>() << endl;
    x = A{ 3,5 };
    cout << x.get_data<A>().x << " " << x.get_data<A>().y << endl;
}
感觉不错?
考虑如何进行拷贝。
对于这样的代码
Any x = 114514;
Any y = x;
我们希望y是复制了x的内容。
Any x = 114514;
Any y = move(x);
如果是move的话,我们希望y直接获得x原本的内容。
struct Any {
    template<typename T>
    Any(const T& t) : ptr{ new Data<T>(t) } {};
    Any(Any&& rhs) noexcept {
        ptr = rhs.ptr;
        rhs.ptr = 0;
    }
    Any& operator=(Any&& rhs) noexcept {
        if (ptr)delete ptr;
        ptr = rhs.ptr;
        rhs.ptr = 0;
        return *this;
    }
    struct Base {

    };
    template<typename T>
    struct Data : Base {
        T data;
        Data(const T& t) : data(t) {}
    };
    Base *ptr;
    template<typename T>
    T& get_data() {
        return ((Data<T>*)ptr)->data;
    }
};
int main() {
    Any x = 114;
    Any y = move(x);
    Any z = 514.1919;
    cout << y.get_data<int>() << endl;
    y = move(z);
    cout << y.get_data<double>() << endl;

}
挺好的,那么如何复制呢?当然,我们也可以显式的给出类型,然后new一个出来,但是这样不够优雅。
所以我们需要引入运行期消耗,也就是虚函数了。
struct Base {
    virtual Base* clone() = 0;
};
template<typename T>
struct Data : Base {
    T data;
    virtual Base* clone() { return new Data<T>(data); }
    Data(const T& t) : data(t) {}
};
这样通过clone就可以new了。
PS:动态类型基本上都需要引入运行期的消耗
修改一下Any,并加上析构函数
struct Any {
    Any() : ptr{ 0 } {}
    template<typename T>
    Any(const T& t) : ptr{ new Data<T>(t) } {};
    Any(const Any& rhs) {
        ptr = rhs.ptr->clone();
    }
    Any& operator=(const Any& rhs) {
        if (ptr)delete ptr;
        ptr = rhs.ptr->clone();
        return *this;
    }
    Any(Any&& rhs) noexcept {
        ptr = rhs.ptr;
        rhs.ptr = 0;
    }
    Any& operator=(Any&& rhs) noexcept {
        if (ptr)delete ptr;
        ptr = rhs.ptr;
        rhs.ptr = 0;
        return *this;
    }
    struct Base {
        virtual Base * clone() = 0;
    };
    template<typename T>
    struct Data : Base {
        T data;
        virtual Base* clone() { return new Data<T>(data); }
        Data(const T& t) : data(t) {}
    };
    Base *ptr;
    template<typename T>
    T& get_data() {
        return ((Data<T>*)ptr)->data;
    }
    ~Any(){
        if (ptr)delete ptr;
    }
};
int main() {
    Any x = 114;
    Any y = x;
    y.get_data<int>() = 514;
    cout << x.get_data<int>() << endl;
    cout << y.get_data<int>() << endl;
}
看上去已经不错了,但是,这个输出有点麻烦了,所以我们重载一下输出。
依然已经有虚函数了,那再加一个也无所谓了。
struct Base {
    virtual Base* clone() = 0;
    virtual ostream& print(ostream& out) = 0;
};
template<typename T>
struct Data : Base {
    T data;
    Data(const T& t) : data(t) {}
    virtual Base* clone() { return new Data<T>(data); }
    virtual ostream& print(ostream& out) {
        out << data;
        return out;
    }
};
ostream& operator << (ostream& out, const Any& oth) {
    oth.ptr->print(out);
    return out;
}
这样就能实现直接输出了。
Any能不能和stl进行嵌套呢?
这里重载一下vector的输出
template<typename T>
ostream& operator << (ostream& out, const vector<T>& v) {
    out << "[";
    for (auto it = v.begin(); it != v.end(); it++) {
        if (it != v.begin())out << ",";
        out << *it;
    }
    out << "]";
    return out;
}
这样来个vector与Any的嵌套
int main() {
    vector<Any> vec = { 114,514,1919,8.10,string("asoul"),string("4soul") };
    cout << vec << endl;
    vector<Any>oth = { string("ygg"),233 };
    vec.push_back(oth);
    cout << vec << endl;
    vec[1] = vector<Any>{ oth,oth };
    cout << vec << endl;
}
输出
[114,514,1919,8.1,asoul,4soul]
[114,514,1919,8.1,asoul,4soul,[ygg,233]]
[114,[[ygg,233],[ygg,233]],1919,8.1,asoul,4soul,[ygg,233]]

有一点需要大家注意


这个地方,如果你使用data{} 来初始话,在msvc 和 gcc 编译器上大概率会报错,但是clang不会...
具体原因之前问过一些群友,好像是  T = vector   data{} 匹配到initializer list 上去了。
另外可能有人觉得,你这个输出的内容好像Json啊,是不是可以用这个来写Json的parser呢?
可以是可以,但对于这个确定了类型的,我们还有variant


还有这个Any类可以优化的,毕竟一个指针加一个虚表就16字节了。


可以在编译器确定类的大小,小的对象就不要new了,当然这可能需要一些模板元编程的知识了。
严格鸽:现代C++学习 模板元编程入门
code
struct Any {
    Any() : ptr{ 0 } {}
    template<typename T>
    Any(const T& t) : ptr{ new Data<T>(t) } {};
    Any(const Any& rhs) {
        ptr = rhs.ptr->clone();
    }
    Any& operator=(const Any& rhs) {
        if (ptr)delete ptr;
        ptr = rhs.ptr->clone();
        return *this;
    }
    Any(Any&& rhs) noexcept {
        ptr = rhs.ptr;
        rhs.ptr = 0;
    }
    Any& operator=(Any&& rhs) noexcept {
        if (ptr)delete ptr;
        ptr = rhs.ptr;
        rhs.ptr = 0;
        return *this;
    }
    struct Base {
        virtual Base * clone() = 0;
        virtual ostream& print(ostream& out) = 0;
    };
    template<typename T>
    struct Data : Base {
        T data;
        Data(const T& t) : data( t ) {}
        virtual Base* clone() { return new Data<T>(data); }
        virtual ostream& print(ostream& out) {
            out << data;
            return out;
        }
    };
    Base *ptr;
    template<typename T>
    T& get_data() {
        return ((Data<T>*)ptr)->data;
    }
    ~Any(){
        if (ptr)delete ptr;
    }
};
ostream& operator << (ostream& out, const Any& oth) {
    oth.ptr->print(out);
    return out;
}
template<typename T>
ostream& operator << (ostream& out, const vector<T>& v) {
    out << "[";
    for (auto it = v.begin(); it != v.end(); it++) {
        if (it != v.begin())out << ",";
        out << *it;
    }
    out << "]";
    return out;
}
int main() {
    vector<Any> vec = { 114,514,1919,8.10,string("asoul"),string("4soul") };
    cout << vec << endl;
    vector<Any>oth = { string("ygg"),233 };
    vec.push_back(oth);
    cout << vec << endl;
    vec[1] = vector<Any>{ oth,oth };
    cout << vec << endl;
}
上一期内容严格鸽:现代C++学习——实现一个std::tuple
下一期就实现variant,与any不同,可以做到运行期基本无消耗(为维护一个type)。我们可以用variant来实现Json
回复

使用道具 举报

0

主题

9

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2023-1-12 12:43:44 | 显示全部楼层
我老早前也介绍过使用虚函数实现类型擦除,以此实现 any 的技法。但是后来我觉得又要 new 对象,又要用虚函数,不利于编译器优化,性能有点低。于是想到了一个更好的办法[惊喜],自己手动实现一个 mtable 类 (相当于手动写了一个虚表结构),any 中塞一个指向 mtable 的指针 (相当于虚表指针),还有一个支持小对象内嵌优化的域。这样做的基本原理还是和用虚函数一样的,但是对于 int 这样的小对象可以直接构造在栈上,避免了动态分配的开销。虚表指针直接在 any 中而不是在 new 出来的对象里,减少了一次寻址的开销。最重要的一点是十分有利于编译器优化,做内联和无用代码消除合并等[惊喜]
回复

使用道具 举报

1

主题

7

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2023-1-12 12:44:15 | 显示全部楼层
https://github.com/WentsingNee/Kerbal/blob/main/include/kerbal/any/any.hpp
回复

使用道具 举报

0

主题

5

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2023-1-12 12:44:44 | 显示全部楼层
代码[惊喜]
回复

使用道具 举报

0

主题

7

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2023-1-12 12:45:38 | 显示全部楼层
我看msvc好像就是自己实现了一个RTTI ,分大小对象的。
rayhunter:C++中VS2019下STL的std::any深入剖析
回复

使用道具 举报

2

主题

4

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2023-1-12 12:46:33 | 显示全部楼层
像这种自己实现std里的东西,需要先看一下官方实现然后在手写一个么?还是直接直接写?[思考](佬
回复

使用道具 举报

4

主题

8

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2023-1-12 12:47:07 | 显示全部楼层
官方的any实现很复杂[捂脸] 一般先在网上看看有没有简易的实现
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表