【问题标题】:How to have a C++ stack with more than one data type?如何拥有具有多种数据类型的 C++ 堆栈?
【发布时间】:2014-03-15 08:40:35
【问题描述】:

问题来了:

我目前正在尝试创建一种简单的基于堆栈的编程语言(反向波兰表示法,FORTH 风格)作为一个更大项目的组件。不过,我遇到了障碍。

在 C++ 中创建包含一种元素类型的堆栈(通过使用 std::vector<>)没有问题(例如,我可以使用语法 std::vector<double> Stack)。

但是,编程语言需要能够保存多种数据类型,例如整数、双精度、字符串和 3D 向量(如具有 X、Y 和 Z 分量的物理向量),仅举一些简单的例子.

那么,在 C++ 中是否有一个结构可以用作堆栈,能够存储多种原始类型/对象/结构?

【问题讨论】:

    标签: c++ string vector stack variant


    【解决方案1】:

    存储不同类型的解决方案是tagged union

    enum Type { INT, STRING, DOUBLE, POINT2D, VECTOR, OBJECT... };
    
    union Data {
        int int_val;
        double double_val;
        struct point2D { int x, int y };
        struct { int v3, int v2, int v1, int v0 }; // you can even use unnamed structs
        // ...
    };
    
    struct StackElem {
        Type type;
        Data data;
    };
    

    在 C++ 中,最好使用 std::variant(或旧 C++ 标准中的 boost::variant),它可能在底层使用标记联合

    但是,当使用反向波兰表示法时,不需要对所有堆栈都使用一个堆栈。你可以use a value stack and a separate operator stack。对于运算符堆栈上的每个运算符,您从值堆栈中弹出相应数量的参数。这将使事情变得更容易并节省内存,因为您可以为运算符使用一个小的 char 数组(除非您需要超过 255 个运算符),并且不会浪费内存来保存 type 以及大于需要的data 上面结构中的字段。这意味着您不需要 OPERATOR 枚举类型中的 Type

    您可以对所有数字类型使用double 类型堆栈,因为双精度型可以包含所有int 类型的范围而不会损失精度。这就是在 Javascript 和 Lua 中实现的。如果运算符需要超过 1 个参数,则只需将它们全部推送/弹出,就像编译器在评估函数时所做的那样。您不再需要担心 int 操作,只需将所有事情都做双重,除非有特定的 int 运算符。但是对于不同的类型,您可能需要不同的运算符,例如 + 用于双重加法,p 或类似的用于矢量加法。但是,如果您需要 64 位 int,则需要单独的整数类型

    例如,如果您需要添加 2 个 3D 向量,请先推第一个向量的 3 个维度,然后再推另一个。当您从运算符堆栈中弹出向量运算符时,从值堆栈中弹出 2 个向量的 3 个维度。对其进行数学运算后,将生成的 3 个维度推入堆栈。不需要向量类型。

    如果您不想将int 存储为double,那么您可以像Firefox 的JS 引擎一样使用NaN-boxing(或nunboxing/punboxing),其中如果值为int,则为64 位的高16为 1,否则为 double(或指针,您可能不会使用)。另一种方法是旧 FFJS 引擎中的type tag in 3 lower bits。在这种情况下,它有点复杂,但您可以对每种类型使用相同的运算符。有关此内容的更多信息,请阅读Using the extra 16 bits in 64-bit pointers

    您甚至可以使用字节数组来存储所有数据类型,并读取运算符指定的正确字节数。例如,如果运算符指示下一个操作数必须是 int,则只需读取 4 个字节。如果是字符串,则先读取字符串长度的 4 个字节,然后从堆栈中读取字符串内容。如果它是int 的二维点,则读取 x 的 4 个字节和 y 的 4 个字节。如果是双读8字节等。这是最节省空间的方式,但显然必须以速度换取

    【讨论】:

    • 但是,当您已经有多个堆栈开始时,问题就出现了,现在每个堆栈实际上都是一堆堆栈。简而言之,它似乎比必要的要复杂一些。
    • 我不明白你的意思。为什么你应该有多个堆栈(实际上在上面的答案中是 2)开始?又为什么是一摞摞?
    • 好吧,如果每个堆栈只有一种类型的元素,那么我需要多个堆栈。这些堆栈必须以某种方式组织起来,因此它们最终会以数组或其他某种组织者的形式出现。现在,如果您为每个程序模拟多个“计算机”,那么您现在将这些“计算机”连同它们的内存一起存储在堆栈中。因此,您最终会得到一堆堆栈数组,或者一堆包含堆栈的对象堆栈(当您第一次听到它时听起来很荒谬,但当您仔细考虑它时它是有道理的)。除非调车场有别的意思,我不明白。
    • 不,您仍然只需要 1 个堆栈来存储数据,请参阅我的编辑。如果您使用标记的联合,那么它与我的答案没有什么不同,除了它需要更多的内存。您仍然必须确定它是什么类型,然后对其进行操作。在我的编辑中使用第一种方式,您甚至不需要区分类型
    • 经过一番阅读、研究和思考,我想我明白你的建议了。但是,我看不到像这样的 16 字节系统节省的地方:float[4], int[4], double[2], char[16], bool[128]。 我看不出你如何在你的例子中处理字符串,这使得事情变得标准和美好。指令从一开始就不会放在堆栈上,所以我看不出将它们放在那里有任何节省。
    【解决方案2】:

    当然,一种方法是使用标记联合:

    enum Type { INTEGER, DOUBLE, /* ... */ };
    
    union Data {
        uint64_t as_integer;
        double as_double;
        // ...
    };
    
    struct Value {
        Type type;
        Data data;
    };
    

    as_integeras_double 等的存储会重叠,因此Value 结构将占用两个字的存储空间,而您的堆栈将具有std::vector<Value> 类型。然后根据type的值访问data的成员:

    void sub(std::vector<Value>& stack) {
        // In reality you would probably factor this pattern into a function.
        auto b = stack.back();
        stack.pop_back();
        assert(b.type == INTEGER);
    
        auto a = stack.back();
        stack.pop_back();
        assert(a.type == INTEGER);
    
        Value result;
        result.type = INTEGER;
        result.data.as_integer = a.data.as_integer - b.data.as_integer;
        stack.push_back(result);
    }
    

    当然,Forths 通常是无类型的,这意味着堆栈只包含单词 (std::vector&lt;uint64_t&gt;),数据值的解释取决于操作它的单词。在这种情况下,您可以通过 union 或 reinterpret_cast 在每个单词的实现中使用适当的类型:

    void subDouble(std::vector<Data>& stack) {
        // Note that this has no type safety guarantees anymore.
        double b = stack.back().as_double;
        stack.pop_back();
    
        double a = stack.back().as_double;
        stack.pop_back();
    
        Data result;
        result.as_double = a - b;
        stack.push_back(result);
    }
    
    void subDouble(std::vector<uint64_t>& stack) {
        double b = reinterpret_cast<double&>(stack.back());
        stack.pop_back();
    
        double a = reinterpret_cast<double&>(stack.back());
        stack.pop_back();
    
        double result = a - b;
        stack.push_back(reinterpret_cast<uint64_t&>(result));
    }
    

    或者,您可以存储的不是值,而是指向类 Value 的实例的指针,其他值类型(例如 IntegerDouble 将从该实例派生):

    struct Value {};
    struct Integer : Value { uint64_t value; };
    struct Double : Value { double value; };
    // ...
    

    您的堆栈类型为std::vector&lt;unique_ptr&lt;Value&gt;&gt;std::vector&lt;Value*&gt;。这样您就不必担心不同的值大小,代价是制作包装器结构并在运行时分配它们的实例。

    【讨论】:

    • 好建议去。在这种情况下你真的需要联合吗?
    • @macroland:我可能会选择标记工会;与基于继承的方法相比,您执行的分配更少,并且您仍然可以进行运行时类型检查以确保安全。并且要对大于一个单词的结构进行操作,您的联合可以包含诸如 Vector3* as_vector 之类的指针。
    • 好的。如果我理解正确的话,有 3 个选项:1. 联合 2. 无类型词 3. 一堆结构。第三个是迄今为止效率最低的,所以我们可以忘记它。第二个似乎是在询问运行时错误,所以我们只剩下联合了。我可以看到工会是如何工作的,但是有没有关于如何使用它们的好教程?
    • 我自己做了一些研究,我想我现在可能会做得更好。但是,在查看之后,我不得不询问对我的代码进行完整性检查的问题。编辑:我想我现在明白了,因为我看到了你改变的例子。
    • 您可以使用 std::variant (C++17) 或 boost::variant 类似 tagged union i> 但更安全(当然,缺点是需要符合 boost 或 C++17 的编译器)。
    【解决方案3】:

    由于 c++ 是一种面向对象的语言,您可以只使用继承。下面是一个取自http://www.cplusplus.com/forum/general/17754/ 的简单示例并进行了扩展:

    #include <iostream>
    #include <vector>
    using namespace std;
    
    // abstract base class
    class Animal
    {
    public:
        // pure virtual method
        virtual void speak() = 0;
        // virtual destructor
        virtual ~Animal() {}
    };
    
    // derived class 1
    class Dog : public Animal
    {
    public:
        // polymorphic implementation of speak
        virtual void speak() { cout << "Ruff!"; }
    };
    
    // derived class 2
    class Cat : public Animal
    {
    public:
        // polymorphic implementation of speak
        virtual void speak() { cout << "Meow!"; }
    };
    
    int main( int argc, char* args[] )
    
        // container of base class pointers
        vector<Animal*> barn;
    
        // dynamically allocate an Animal instance and add it to the container
        barn.push_back( new Dog() );
        barn.push_back( new Cat() );
    
        // invoke the speak method of the first Animal in the container
        barn.front()->speak();
    
        // invoke all speak methods and free the allocated memory
        for( vector<Animal*>::iterator i = barn.begin(); i != barn.end(); ++i )
        {
            i->speak();
            delete *i;
        }
        // empty the container
        barn.clear();
    
        return 0;
    }
    

    【讨论】:

    • 如果 float、double 和 string 都是自定义类,也许。这如何与 int、double、string 和其他我无法控制的类一起使用?
    • 您可以为基元构建包装类并将它们放入向量中。我不知道您到底想对堆栈中的项目做什么,但这可能对您程序的其他部分也有帮助。
    • 应该只为那些浏览的人添加它 - @StefanNeubert 使用 vector&lt;Animal*&gt; 而不是 vector&lt;Animal&gt; 来防止称为对象切片的东西。 en.wikipedia.org/wiki/Object_slicing
    【解决方案4】:

    我建议使用继承。为需要存储的对象创建通用基类,并创建基类型向量。将所有继承对象存储在此向量中。

    【讨论】:

    • 如果您尝试存储原始类型,这将如何工作?据我所知,原语没有超类。
    • 您必须至少为该值创建结构。看看 Jon Purdy 的答案,尤其是它的第二部分。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-17
    • 1970-01-01
    相关资源
    最近更新 更多