可以从std::string继承以提供类型一致性吗

Is it ok to inherit from std::string to provide type consistency?

本文关键字:类型 一致性 string std 继承      更新时间:2023-10-16

我想要一个类来存储团队的名称。我可以使用std::字符串,但它在语义上并不正确。std::string表示任何字符串,而我的团队名称不能为空或超过10个字符。

class TeamName {
public:
TeamName(std::string _teamName) : teamName(std::move(_teamName))
{
if (teamName.length() == 0 || teamName.length() > 10) {
throw std::invalid_argumet("TeamName " + teamName + " can't be empty or have more than 10 chars." );
}
}
private:
std::string teamName;
};

我喜欢这样:我只检查一次名称的完整性,从未听说过这一要求的未来开发人员将无法从他们的新函数中传播无效名称,在程序逻辑中有一个明显的位置来放置异常处理程序等。

但这种构造意味着我必须使用getter和setter来与名称交互,这会对代码造成很大污染。然而,如果我从std::string继承,代码几乎不会改变,我可以将TeamName完全视为std::string

class TeamName : public std::string {
public:
TeamName(std::string _teamName) : std::string(_teamName)
{
if (length() == 0 || length() > 10) {
throw std::invalid_argumet("TeamName " + teamName + " can't be empty or have more than 10 chars." );
}
}
};

这种做法不好吗?这在概念上对我来说是正确的。

有什么方法可以实现我想要的一致性,并且仍然能够使用与使用字符串没有区别的包装器?

在C++17中,您可以使用std::string_view作为隐式转换的目标类型:

class TeamName {
std::string val;
public:
TeamName(std::string name): val(std::move(name)) { /* snip */ }
operator std::string_view() const {
return val;
}
};

这使您可以在任何可以使用std::string_view的地方使用TeamName,同时不允许修改(因此在创建对象后,长度不变)。当然,这需要代码中应该使用对象的部分了解std::string_view

如果std::string_view对您来说不可行,您可以将转换定义为const std::string&

继承的一个潜在问题是,它让人们将您的TeamName视为正常的std::string。示例:

class TeamName : public std::string {
public:
TeamName(std::string _teamName) : std::string(_teamName)
{
if (length() == 0 || length() > 10) {
throw std::invalid_argument("TeamName " + *this + " can't be empty or have more than 10 chars.");
}
}
};
void appendToString(std::string& s)
{
s.append(" this is a long text, and the resulting teamname is no longer valid...");
}
int main()
{
TeamName t = std::string("CoolTeam");
std::cout << "The teamname is: " << t << std::endl;
appendToString(t);
std::cout << "And now it is: " << t << std::endl;
}

输出:

The teamname is: CoolTeam
And now it is: CoolTeam this is a long text, and the resulting teamname is no longer valid...

保护您继承的TeamName不受所有非法更改的影响是可能的-只需重载.append和所有其他可以使其无效的函数即可。但对于继承,问题在于找到所有必须检查/限制的函数,然后编写一个重载来处理它。

EDIT:不正确,正如评论中所指出的,std::string中的成员函数没有标记为virtual。这意味着,如果您通过指向std::string的指针或引用访问TeamName,则获得std::string实现,再多的重载也无法保护您的TeamName不受修改。

另一方面,使用私人字符串,人们只能通过您允许他们访问的功能来访问您的数据。

实际上,这取决于派生类TeamName的使用方式。

您只需要知道,std::basic_string没有virtual析构函数(edit:并且不是多态的)。


因此,如果您尝试使用多态性(即通过std::string对象访问TeamName对象),可能会出现问题。

事实上(例如),以下内容:

std::string * ms = new TestName; // Polymorphism
delete ms;                       // Undefined behaviour

这里,当您调用delete时,只会调用std::string的析构函数。

问题是,由于在这种情况下永远不会调用TestName的析构函数,因此打开了内存泄漏的大门
此外,通过指向基(没有virtual析构函数)的指针删除派生对象是未定义的行为。你可能有兴趣看看这个SO的答案。

这就是为什么,为了避免出现问题,一条经验法则是永远不要从没有被标记为virtual的析构函数的基类继承。


但是,如果您根本不打算使用多态性,那么让TestName继承std::string应该是可以的。

编辑:但我不建议您这样做,因为您无法防止将来使用您的代码的任何人滥用您的类(例如,将其提供给一个需要std::string&的函数,因为它也是多态性)。


编辑:(关于为什么我们不能使用多态性的详细信息)

当然,virtual析构函数的缺乏并不是故事的全部。我们不能使用多态性,因为std::string不是多态性

事实上,为了使用多态性,基类需要是多态的
换句话说,基类至少需要一个virtual成员函数(当然可以是析构函数)。

如果未满足此要求,则不会为此结构发出vtable(虚拟表)
因此,如果我们无论如何都试图使用多态性,无论是通过指针还是通过引用,如果我们试图通过非多态基访问它,就永远找不到真正的对象类型和实现(没有vtable,动态类型解析是不可能的)
此外,另一个后果是,我们也将受到对象切片的约束(派生对象成员的所有内容都将丢失/切片)。

std::string不意味着从公共继承。你可以私下继承,但私下继承或多或少和组合一样,没有太大的优势。

话虽如此,这并不是最糟糕的做法(即使是公共继承在谨慎使用时也可以),但我认为你把事情搞得过于复杂了。类的一项主要工作是确保其不变量在任何时候都保持不变。如果您有一个以名称为成员的Team,并且该名称具有某些不变量(不超过10个字符,没有空字符串),那么拥有一个std::string成员并让Team处理这些约束也是完全可以的。

有时您只需要检查约束一次。假设Team在构造时获得了一个名称,之后该名称就无法更改。在这种情况下,以下是我有时使用的模式:

#include <string>
#include <iostream>
#include <stdexcept>
struct TeamName {
std::string value;
TeamName(std::string value) : value(value) {
if (value.length() == 0 || value.length() > 10) {
throw std::invalid_argument("TeamName " + value + " can't be empty or have more than 10 chars." );
}
}
operator std::string() { return value; }
};
struct Team {        
Team(TeamName name) : name(name) {}
private:
std::string name; 
};
int main() 
{
try {
Team x{std::string("")};
} catch (std::exception& e) { std::cout << e.what(); }
}

CCD_ 38的用户和CCD_。而且两者都使用普通字符串。TeamName仅用于验证。

我只检查过一次名称的完整性,从未听说过这一要求的未来开发人员将无法从他们的新函数中传播无效名称,在程序逻辑中有一个明显的位置来放置异常处理程序等。

因此,用public继承类是个坏主意,因为这将公开所有字符串操作函数,并且您需要确保它们都不会破坏您的约束,例如:

TeamName test;
test.append( … ); // could make the name longer then 10
test.clear(); // name is 0

您可以覆盖这些当前函数中的每一个,并添加约束检查,但如果您忘记了一个呢?

或者您使用可见性protected进行继承,并且只公开您需要的函数,但它就像一个包装器,所以您没有从继承中获得任何好处。

因此,在这种情况下,我看到了从string继承的更多缺点。