这是不多态继承的一个好理由吗?

5

std::string(以及所有标准类)都没有任何虚方法,因此创建一个带有虚方法的继承类将导致 UB(最可能是由于析构函数)。 (如果我错了,请纠正我)。

我曾认为在没有多态性的情况下进行继承是可以的,直到我在网上阅读了这个主题。

例如,在这个答案中:Why should one not derive from c++ std string class?给出了一些反对这种做法的论点。主要原因似乎是“切片问题”,当派生对象在代替std::string参数传递给函数时,这将禁止添加功能,从而使非多态性不合理。惯用的C ++方式是创建自由函数,如果想要扩展string的功能。我同意所有这些观点,尤其是因为我支持自由函数而不是单体类。


话虽如此,我认为我找到了一种情况,我认为实际上有必要从std::string进行非多态继承。首先,我将展示我正在尝试解决的问题,然后我将展示为什么我认为从std::string继承是最佳解决方案。

将一个用于调试目的的函数从C移植到C ++时,我意识到没有办法在C ++中创建格式化字符串(不使用类似于C的字符串函数,例如sprintf)。也就是说:

C版本:void someKindOfError(const char *format, ...);
这将被调用:

someKindOfError("invalid argument %d, size = %d", i, size);

C++版本:void someKindOfError(const std::string &message);
调用此函数类似于:

std::stringstream ss;
ss << "invalid argument " << i << ", size = " << size;
someKindOfError(ss.str());

这个不可能只是一行代码,因为 << 操作符返回一个 ostream。所以需要额外两行代码和一个变量。

我想到的解决方案是创建一个名为 StreamString 的类,它继承自 std::string(实际上是继承自 templated 的 BasicStreamString,它又继承自 basic_string<>,但这并不重要),在新功能方面,它具有类似于 stringstream 操作符的<<操作符,以及转换成和转换自 string 的功能。

因此,前面的示例可以变为:

someKindOfError(StreamString() << "invalid argument " << i << ", size = " << size);

请记住,参数类型仍为const std::string&

该类已创建并完全可用。当需要即席创建字符串而不必声明额外的stringstream变量时,我发现这个类在很多地方非常有用。这个对象可以像stringstream一样进一步操作,但它实际上是一个string,可以传递给期望string的函数。


我为什么认为这是C++惯用法的例外:

  • 当传递给期望string的函数时,该对象需要完全像一个string一样运作,因此切片问题不是一个问题。
  • 唯一(值得注意的)添加的功能是operator<<,我不愿意为标准string对象重载它作为自由函数(这将在库中完成)。

我能想到的一个替代方法是创建一个可变参数模板自由函数。类似于:

template <class... Args>
std::string createString(Args... args);

这允许我们像这样进行调用:

someKindOfError(createString("invalid argument ", i , ", size = " , size));

这种替代方法的一个缺点是失去了像 stringstream 那样在创建后轻松操作字符串的能力。但我想我也可以创建一个自由函数来处理它。此外,人们通常使用运算符<< 来执行格式化插入。
总之:
  • 我的解决方案是不是不好的实践(或最差的)?还是它是C++习语的例外,是可以接受的?
  • 如果不好,有哪些可行的替代方案?createString 可以吗?它能被改进吗?

创建一个带有虚成员函数的派生类并不一定会导致未定义行为。无论派生类是否具有虚成员函数,通过指向基类的指针删除派生类的实例都会导致未定义行为。此外,您的StreamString不需要从std::string派生才能以这种方式使用。只需为其提供到std::stringchar const*的转换运算符即可。 - Andy Prowl
1
@HansPassant:在C++11中有final(上下文)关键字。 - Andy Prowl
是的,终于 :) 对于 std::string 来说有点晚了。 - Hans Passant
为什么要在字符串上使用运算符<<?你的“someKindOfError”实际上是一个流!因此,我的回答是:不要这样做。 - user2249683
2个回答

4
您不需要从std::string派生出一个类来实现这个功能。只需创建一个与之无关的类,例如StringBuilder,它在内部保持一个std::stringstream。为这个类重载运算符<<,并添加一个std::string类型的强制转换运算符即可。
下面的代码片段应该可以解决问题(未经测试):
class StringBuilder
{
    std::ostringstream oss;

public:
    operator std::string() const
    {
        return oss.str();
    }

    template <class T>
    friend StringBuilder& operator <<(StringBuilder& sb, const T& t)
    {
        sb.oss << t;
        return *this;
    }
};

1
这只是一个带有转换运算符的std::ostringstream而已。(我担心没有任何收益 - 甚至连方便性都没有) - user2249683
2
@DieterLücking:不只是这样。在std::ostringstream中的operator<<返回了一个ostream &,因此在调用.str()之前,返回值必须被强制转换回ostringstream & - Mankarse
我使用继承而不是一个无关的类的原因是,我不必重写所有字符串接口(如大小、operator[]、词典比较运算符等)。一个只提供字符串提取选项的基本类是否可行(而不是提供字符串接口)? - bolov
@DieterLücking 是的,这只是一些花言巧语。这就是问题所要求的。 - D Drmmr
@bolov 你会如何使用字符串接口?从你的问题来看,似乎你只是想使用流接口构建一个字符串,并将其在单行中传递给函数。 - D Drmmr
显示剩余3条评论

1
你的 StreamString 类是可以的,因为在正常使用中似乎没有任何可能导致问题的情况。即便如此,仍有很多其他更适合这种情况的替代方案。
  1. 使用现有的库,例如Boost.Format,而不是自己编写。这样做的好处是广为人知,经过测试,得到支持等等...

  2. someKindOfError编写为可变参数模板,以匹配C版本,但增加了C ++类型安全性。这样做的好处是与C版本匹配,因此对您现有的用户来说更加熟悉。

  3. StringStream添加转换运算符或显式的to_string函数,而不是继承自std::string。这样可以使您在以后更灵活地更改StringStream的实现。(例如,在以后的某个阶段,您可能决定要使用某种缓存或缓冲方案,但如果您不知道最终字符串何时从StringStream中提取,这将是不可能的)。

    目前,您的设计在概念上存在缺陷。您唯一需要的是将StringStream转换为std::string的能力。相比于使用转换运算符,继承是一种过于笨重的实现方式。

  4. 将原始的stringstream代码编写为一行:

someKindOfError(static_cast<std::stringstream &>(
    std::stringstream{} << "invalid argument " << i << ", size = " << size).str());

...嗯,那很丑陋,也许不要这样做。但是,如果你不这样做的唯一原因是认为它不可能,那么你应该考虑一下。


我使用继承而不是一个没有关联的类和转换运算符(之前我认为这可能会有问题)的原因是,我不必重写所有的string接口(如sizeoperator[]、字典序比较运算符等)。一个只提供字符串提取选项的基本类是否可行(而不是提供字符串接口)? - bolov
@bolov:在调用someKindOfError时,您在哪里使用string接口?更一般地说,您何时何地需要一个具有字符串接口的StringStream?似乎给它一个字符串接口会带来不利而非优势。 - Mankarse
这个想法最初的起源是因为我想将依赖于某些变量值的字符串传递给接收std::string的函数(比如我的someKindOfError示例),但现在我认为它就像是具有stringstringstream的最佳特性。 - bolov
一个 string 和一个 ostream 在本质上是不同的。一个字符串允许在字符缓冲区内任意索引和修改字符,而 ostream 允许将某个对象的格式化版本附加到字符缓冲区的末尾(仅限末尾)。为这两种情况提供单独的类提供了关注点的分离,允许更灵活地实现这两个类,并澄清使用它们的代码的含义。 - Mankarse
如果你真的认为“任意可修改的字符缓冲区,同时还能在其末尾提供格式化插入”是一个有用的概念,那么你仍然不应该继承自std::string,因为这样做会不必要地限制你的实现选项,唯一的优点就是节省了一点打字时间。 - Mankarse
只有在你从中继承的类被设计为以那种方式使用时,继承才能获得实现。比如没有状态的小类(例如std::true_type),CRTP基类(但即使是这些也有点问题),或者是某个类型的私有实现中不可能以其他方式实现的类(例如在实现std::tuple时存在的一些类)。 - Mankarse

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