C++中代数数据类型的等价物是什么?

19

假设我有以下的Haskell代码:

data RigidBody = RigidBody Vector3 Vector3 Float Shape -- position, velocity, mass and shape
data Shape = Ball Float -- radius
           | ConvexPolygon [Triangle]

在C++中,最好的表达方式是什么?

struct Rigid_body {
    glm::vec3 position;
    glm::vec3 velocity;
    float mass;
    *???* shape;
};

我想问的是,在结构体中如何表示形状,当它可以是两种类型之一。


2
什么是问题?如何引用尚未定义的不同类型“Shape”?如何创建一个类型“Shape”,它实际上是一组其他类型之一?以上所有内容?您可能需要考虑研究不同的编程范例,直接将Haskell翻译成C ++很可能不会很好地工作... - David Rodríguez - dribeas
我认为最简单的方法是使用结构体的联合,但我宁愿尝试编写Haskell解析器,也不想在C++中编写任何程序。 - dfeuer
1
我想问的是如何将这种“variant”类型翻译成C++所使用的任何范例。请参见我的编辑。 - user3133295
2
@user3133295:恕我直言,最好先学习C++,然后再尝试解决这个问题,而不是在没有了解C++习语的情况下尝试将Haskell翻译成C++。 - Oliver Charlesworth
4个回答

15

在C++中,有不同的方法可以用来解决这个问题。

纯面向对象的方法是定义一个接口Shape,并将两种不同的选项作为实现该接口的派生类型。然后,RigidBody将包含指向Shape的指针,该指针将被设置为引用BallConvexPolygon之一。优点:人们喜欢面向对象编程(不确定这是否真的是优点:)),易于扩展(您可以稍后添加更多形状而无需更改类型)。缺点:您应该为Shape定义一个适当的接口,它需要动态分配内存。

放弃面向对象,您可以使用boost::variant或类似类型,它基本上是一个标记联合,将保存其中一个类型。优点:没有动态分配,形状局部于对象。缺点:不是纯面向对象的(人们喜欢面向对象,您没忘吧?),不太容易扩展,不能通用地使用形状。


1
我并不在乎通用性。我不想要求我的“刚体”创建者手动分配内存。我宁愿为刚体制造两个构造函数,然后构造形状本身。 - user3133295
2
@user3133295:在C++中的构造函数并不像在Haskell中的“构造函数”那样。如果你使用C++,最终你还是需要进行内存分配,但这对用户来说并不是一个可怕的负担(例如 RigidBody 的构造函数可以接受一个 Shape*)。实际上,在C++中,面向对象编程方法比 boost::variant 方法更加普遍。如果你使用动态分配的内存,请记得使用智能指针来自动释放内存,以减轻手动内存管理的负担。 - David Rodríguez - dribeas
1
如果我不想让这两个形状有任何共同之处怎么办?我无法将Shape转换为Ball,因为显然Shape不是多态类型。 - user3133295
1
如果你想使用多态性,你需要使你的类型具有多态性。如果没有其他原因,也要能够在某个时候“删除”对象! - David Rodríguez - dribeas
我不明白为什么1)面向对象编程在C++中是多范式的,所以根本无关紧要;2)boost::variant怎么可能不是面向对象的(在继承方面没有意义)。 - user1804599
Boost variant 不够高效,因为它会双重分配所有内存,包括至少一个在堆上的内存,即使 variant 在栈上。 - johnbakers

13

还有一种可能性是使用 boost::variant,它将在C++17中作为 std::variant 添加到标准库中:

struct Ball { float radius; };
struct ConvexPolygon { Triangle t; }

using Shape = boost::variant<Ball, ConvexPolygon>;

这种方法的优点:
  • 与标记联合不同,是类型安全的
  • 可以持有复杂类型,不像联合体
  • 与OO不同,不需要所有“子”类型具有统一接口
缺点如下:
  • 有时需要在访问变量时进行类型检查以确认它是您想要的类型,不像OO
  • 需要使用boost或C++17兼容;这可能对某些编译器或某些组织而言很困难,因为OO和联合体得到了普遍支持

2
在C++中,规范的做法是采用Justin Wood所提供的基于继承的解决方案。规范地,您可以为每种Shape赋予虚函数。
但是,C++也有union类型。您可以使用"tagged unions"。
struct Ball { /* ... */ };
struct Square { /* ... */ };
struct Shape {
  int tag;
  union {
    Ball b;
    Square s;
    /* ... */
  }
};

你可以使用tag成员来指定Shape是一个Ball还是一个Square或其他任何形状。你可以在tag成员上使用switch等操作。
这种方法的缺点是,Shape比最大的BallSquare和其他形状都多一个int;在OCaml等对象中不存在此问题。
你使用哪种技术取决于如何使用Shape

3
在C++03中,联合类型的使用受到限制且不太方便。如果联合体中的任何元素不是POD类型,则需要添加自己的构造函数、复制构造函数和赋值运算符,并手动管理对象的生命周期。这就是boost::variant为您解决的问题。在C++11中,联合类型的使用方式变得更加笨拙。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas:是的。我无法想象为什么“Ball”或“Square”不会是POD。 - tmyklebu
在 Haskell 代码中,它不是 Square,而是 ConvexPolygon,它可能具有可变数量的顶点。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas:收到。 - tmyklebu
可怕!这种方法肯定会导致疯狂。你正在通过联合和整数标签重新发明多态性,然后信任其他开发人员遵循访问元素的规则。使用OO层次结构,如果需要专门的方法并且不能通过虚函数进行泛化,则使用dynamic_cast到专门的类型将为您提供联合设计所不具备的类型安全性。 - Nathan
1
@Nathan:是的,你可以使用一堆 dynamic_cast。但我不知道如何在对象的动态类型上进行 switch - tmyklebu

1
你需要创建一个基类Shape。从这里开始,你可以创建实际的形状类BallConvexPolygon。你需要确保BallConvexPolygon是基类的子类。
class Shape {
    // Whatever commonalities you have between the two shapes, could be none.
};

class Ball: public Shape {
    // Whatever you need in your Ball class
};

class ConvexPolygon: public Shape {
    // Whatever you need in your ConvexPolygon class
};

现在,您可以像这样创建一个通用对象。
struct Rigid_body {
    glm::vec3 position;
    glm::vec3 velocity;
    float mass;
    Shape *shape;
};

当你实际初始化shape变量时,可以使用BallConvexPolygon类进行初始化。您可以继续创建任意数量的形状。


2
你可能希望 Rigid_body 拥有一个 Shape * 而不是一个 Shape,这样你就可以避免切片问题。 - tmyklebu
已经修复。 - Justin Wood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接