为什么不能从c ++ std字符串类派生?

时间:2011-05-15 06:26:30

标签: c++ string inheritance stl

我想问一下有效C ++中的一个特定点。

它说:

  

如果一个类需要像多态类一样运行,那么析构函数应该是虚拟的。它进一步补充说,由于std::string没有虚拟析构函数,因此永远不应该从中派生出来。另外std::string甚至没有被设计为基类,忘记了多态基类。

我不明白一个类中具体要求什么才有资格成为基类(不是多态的)?

我不应该从std::string类派生的唯一原因是它没有虚拟析构函数吗?为了可重用性,可以定义基类,并且多个派生类可以从中继承。那么是什么使得std::string甚至没有资格成为基类?

此外,如果存在纯粹为可重用性定义的基类并且有许多派生类型,是否有任何方法可以阻止客户端执行Base* p = new Derived(),因为这些类不应该以多态方式使用?

8 个答案:

答案 0 :(得分:55)

我认为这句话反映了这里的混乱(强调我的):

  

我不明白课程中具体要求什么才有资格成为基础课程(不是多态的)?

在惯用的C ++中,从类派生有两种用途:

  • 私有继承,用于使用模板进行mixins和面向方面编程。
  • 公开继承,仅用于多态情境编辑:好的,我想这可以在一些mixin场景中使用 - 例如boost::iterator_facade - 在CRTP正在使用时显示。

如果你没有尝试做多态的事情,绝对没有理由在C ++中公开派生一个类。该语言附带免费功能作为该语言的标准功能,免费功能是您应该在这里使用的。

以这种方式思考 - 您是否真的想强制您的代码客户端转换为使用某些专有字符串类只是因为您想要使用几种方法?因为与Java或C#(或大多数类似的面向对象语言)不同,当您在C ++中派生类时,大多数基类用户需要知道这种更改。在Java / C#中,类通常通过引用访问,类似于C ++的指针。因此,存在一定程度的间接性,它将类的客户端分离,允许您在没有其他客户知道的情况下替换派生类。

但是,在C ++中,类是值类型 - 与大多数其他OO语言不同。最简单的方法就是所谓的the slicing problem。基本上,请考虑:

int StringToNumber(std::string copyMeByValue)
{
    std::istringstream converter(copyMeByValue);
    int result;
    if (converter >> result)
    {
        return result;
    }
    throw std::logic_error("That is not a number.");
}

如果您将自己的字符串传递给此方法,则会调用std::string的复制构造函数来制作副本,不是派生对象的复制构造函数 - 无论什么传递了std::string的子类。这可能导致您的方法与附加到字符串的任何内容之间的不一致。函数StringToNumber不能简单地取任何派生对象并复制它,因为派生对象的大小可能与std::string不同 - 但是这个函数被编译为仅保留空间自动存储中的std::string。在Java和C#中,这不是问题,因为涉及自动存储的唯一事情是引用类型,并且引用总是相同的大小。在C ++中并非如此。

长话短说 - 不要使用继承来处理C ++中的方法。这不是惯用语,会导致语言出现问题。尽可能使用非朋友,非成员函数,然后使用合成。除非您是模板元编程或想要多态行为,否则不要使用继承。有关更多信息,请参阅Scott Meyers的Effective C++第23项:将非成员非朋友函数更喜欢成员函数。

编辑:这是一个更完整的示例,显示切片问题。您可以在codepad.org

上看到它的输出
#include <ostream>
#include <iomanip>

struct Base
{
    int aMemberForASize;
    Base() { std::cout << "Constructing a base." << std::endl; }
    Base(const Base&) { std::cout << "Copying a base." << std::endl; }
    ~Base() { std::cout << "Destroying a base." << std::endl; }
};

struct Derived : public Base
{
    int aMemberThatMakesMeBiggerThanBase;
    Derived() { std::cout << "Constructing a derived." << std::endl; }
    Derived(const Derived&) : Base() { std::cout << "Copying a derived." << std::endl; }
    ~Derived() { std::cout << "Destroying a derived." << std::endl; }
};

int SomeThirdPartyMethod(Base /* SomeBase */)
{
    return 42;
}

int main()
{
    Derived derivedObject;
    {
        //Scope to show the copy behavior of copying a derived.
        Derived aCopy(derivedObject);
    }
    SomeThirdPartyMethod(derivedObject);
}

答案 1 :(得分:24)

答案 2 :(得分:10)

析构函数不仅不是虚拟的,std :: string根本不包含虚拟函数 ,也没有受保护的成员。这使派生类很难修改其功能。

那你为什么要从中得到它?

非多态的另一个问题是,如果将派生类传递给期望字符串参数的函数,那么您的额外功能将被切掉,对象将再次被视为纯字符串。

答案 3 :(得分:9)

如果真的想要从中派生(不讨论你为什么要这样做),我认为你可以通过使Derived成为operator new类直接堆实例化来阻止它私人:

class StringDerived : public std::string {
//...
private:
  static void* operator new(size_t size);
  static void operator delete(void *ptr);
}; 

但是这样你就可以将自己限制在任何动态StringDerived对象中。

答案 4 :(得分:4)

  

为什么不能从c ++ std字符串类派生出来?

因为不必要。如果您想使用DerivedString进行功能扩展;我在导出std::string时没有看到任何问题。唯一的问题是,你不应该在两个类之间进行交互(即不要使用string作为DerivedString的接收者。)

  

有没有办法阻止客户做Base* p = new Derived()

即可。确保在inline类中的Base方法周围提供Derived个包装器。 e.g。

class Derived : protected Base { // 'protected' to avoid Base* p = new Derived
  const char* c_str () const { return Base::c_str(); }
//...
};

答案 5 :(得分:2)

不能从非多态类导出有两个简单的原因:

  • 技术:它引入了切片错误(因为在C ++中我们通过值传递,除非另有说明)
  • 功能性:如果它是非多态的,你可以通过合成和一些功能转发实现相同的效果

如果您希望向std::string添加新功能,请首先考虑使用免费功能(可能是模板),例如Boost String Algorithm库。

如果您希望添加新数据成员,请通过在类设计的类中嵌入类组件来正确包装类访问。

修改

@Tony正确地注意到我引用的功能原因对大多数人来说可能毫无意义。在一个好的设计中有一个简单的经验法则,即当你可以在几个中选择一个解决方案时,你应该考虑具有较弱耦合的解决方案。组合具有较弱的耦合,即继承,因此应该是可取的。

此外,组合使您有机会很好地包装原始的类方法。如果你选择继承(公共)并且方法不是虚拟的(这里就是这种情况),这是不可能的。

答案 6 :(得分:0)

C ++标准规定,如果基类析构函数不是虚拟的,并且您删除了指向派生类对象的Base类对象,则会导致未定义的行为。

C ++标准第5.3.5 / 3节:

如果操作数的静态类型与其动态类型不同,则静态类型应为操作数的动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。

要明确非多态类&amp;需要虚拟析构函数
使析构函数虚拟化的目的是通过delete-expression促进对象的多态删除。如果没有对象的多态删除,那么您不需要虚拟析构函数。

为什么不从字符串类派生?
一般应该避免从任何标准容器类派生,因为他们没有虚拟析构函数,这使得无法以多态方式删除对象。 对于字符串类,字符串类没有任何虚函数,因此没有什么可以覆盖的。你能做的最好的事情就是隐藏一些东西。

如果您想拥有类似字符串的功能,您应该编写自己的类,而不是从std :: string继承。

答案 7 :(得分:0)

一旦将任何成员(变量)添加到派生的std :: string类中,如果您尝试将std goodies与派生的std :: string类的实例一起使用,是否会系统地拧栈?因为stdc ++函数/成员将其堆栈指针[索引]固定(并调整为)(基本std :: string)实例大小的大小/边界。

对吗?

请纠正我的错误。