9

我有一个由嵌套 STL 容器组成的数据结构:

typedef std::map<Solver::EnumValue, double> SmValueProb;
typedef std::map<Solver::VariableReference, Solver::EnumValue> SmGuard;
typedef std::map<SmGuard, SmValueProb> SmTransitions;
typedef std::map<Solver::EnumValue, SmTransitions> SmMachine;

这种形式的数据仅在我的程序中短暂使用,除了简单地存储它们的数据之外,附加到这些类型并没有太多有意义的行为。但是,编译器 (VC++2010) 抱怨生成的名称太长。

将类型重新定义为 STL 容器的子类,无需进一步说明似乎可行:

typedef std::map<Solver::EnumValue, double> SmValueProb;
class SmGuard : public std::map<Solver::VariableReference, Solver::EnumValue> { };
class SmTransitions : public std::map<SmGuard, SmValueProb> { };
class SmMachine : public std::map<Solver::EnumValue, SmTransitions> { };

认识到 STL 容器不打算用作基类,在这种情况下实际上是否存在任何危险?

4

3 回答 3

12

有一个危险:如果你调用delete一个指向没有virtual析构函数的基类的指针,你就会有未定义的行为。否则,你很好。

至少理论是这样的。在实践中,在 MSVC ABI 或 Itanium ABI(gcc,Clang,icc,...)delete中,没有虚拟析构函数的基类(-Wdelete-non-virtual-dtor使用 gcc 和 clang,提供类具有虚拟方法)只会导致问题,如果您派生类添加具有非平凡析构函数的非静态属性(例如 a std::string)。

在您的具体情况下,这似乎很好......但是......

...您可能仍想封装(使用组合)并公开有意义的(面向业务的)方法。it->second.find('x')->begin()它不仅危险性更小,而且比...更容易理解。

于 2012-11-26T07:49:43.970 回答
1

就在这里:

std::map<Solver::VariableReference, Solver::EnumValue>* x = new SmGuard;
delete x;

导致未定义的行为。

于 2012-11-26T07:23:42.483 回答
1

这是 C++ 与“基于继承的经典 OOP”的争议点之一。

有两个方面必须考虑:

  • typedef 为同一类型引入了另一个名称:std::map<Solver::EnumValue, double>并且SmValueProb- 在所有效果上 - 完全相同并且可以互换使用。
  • 一个引入了一种(原则上)与其他任何东西无关的新类型。

类关系由类“组成”的方式定义,以及是什么让隐式操作和转换成为可能与其他类型。

在特定的编程范式(如 OOP,与“inhritance”和“is-a”关系的概念相关联)继承、隐式构造函数、隐式转换等之外,都做同样的事情:让一个类型被使用跨另一种类型的接口,从而定义了跨不同类型的可能操作网络。这是(一般来说)“多态性”。

存在各种编程范式来说明应该如何构建这样的网络,每个都试图优化编程的特定方面,例如表示或运行时可替换对象(经典 OOP)、编译时可替换对象(CRTP)的表示、使用不同类型的流派算法函数(通用编程),使用“纯函数”来表示算法组合(函数和 lambda“捕获”)。

所有这些都规定了一些关于必须如何使用语言“特性”的“规则”,因为 - 作为 C++ 多范式 - 其特性仅满足范式的要求,从而使一些肮脏的东西变得开放。

正如Luchian所说,继承 std::map 不会产生纯 OOP 可替换类型,因为对基指针的删除将不知道如何销毁派生部分,因为析构函数不是设计为虚拟的。

但是 - 事实上 - 这只是一个特例:也pbase->find不会调用你自己的最终覆盖的find方法,std::map::find不是虚拟的。(但这不是未定义的:它被很好地定义为很可能不是您想要的)。

真正的问题是另一个:“经典 OOP 替代原则”在您的设计中是否重要?换句话说,你是否打算互换使用你的类和它们的基础,函数只接受一个std::map*orstd::map&参数,假装这些函数调用 std::map 函数导致调用你的方法?

  • 如果是,继承不是要走的路。std::map 中没有虚方法,因此运行时多态性将不起作用。
  • 如果不是,那就是:您只是在编写自己的类,重用 std::map 行为和接口,无意交换它们的用法(特别是,您不是用 new 分配自己的类,并用 delete 应用删除它们到 std::map 指针),只提供一组以yourclass&yourclass*作为参数的函数,这很好。它甚至可能比 typedef 更好,因为您的函数不能再与 std::map 一起使用,从而分离了功能。

替代方案可以是“封装”:即:使地图和类的显式成员允许地图作为公共成员访问,或者使其成为具有访问器函数的私有成员,或者自己重写类中的地图接口。你最终得到了一个不相关的类型,它具有相同的接口和它自己的行为。以重写可能有数百个方法的东西的整个接口为代价。

笔记:

对于任何考虑丢失 vitual dtor 的危险的人来说,注意 tat 包含公众可见性并不能解决问题:

class myclass: public std::map<something...>
{};

std::map<something...>* p = new myclass;
delete p;

UB 非常喜欢

class myclass
{
public:
   std::map<something...> mp;
};

std::map<something...>* p = &((new myclass)->mp);
delete p;

第二个示例与第一个示例有相同的错误,只是不太常见:它们都假装使用指向部分对象的指针来操作整个对象,部分对象中没有任何内容让您能够知道“包含一”是。

于 2012-11-26T08:18:34.043 回答