可以从std::string继承以提供类型一致性吗
Is it ok to inherit from std::string to provide type consistency?
我想要一个类来存储团队的名称。我可以使用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
继承的更多缺点。
- ArduinoJson 6.15.2:JsonObject没有命名类型
- 防止主数据类型C++的隐式转换
- 大量序列中核苷酸类型的快速计数
- 如何从C++中的依赖类型中获得它所依赖的类型
- 有关插入适配器的错误。[错误]请求从 'back_insert_iterator<vector<>>' 类型转换为非标量类型
- 是否可以初始化不可复制类型的成员变量(或基类)
- 如何获取std::result_of函数的返回类型
- 从父命名空间重载类型
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 我想将一个对T类型的非常量左值引用绑定到一个T类型的临时值
- Openssl 1.1.1d无效使用不完整的类型"struct dsa_st"
- 访问者访问变体并返回不同类型时出错
- 在VS2010-VS2015下编译时,如何使用decltype作为较大类型表达式的LHS
- 处理小于cpu数据总线的数据类型.(c++转换为机器代码)
- C++ 雷神库 - 使用资源加载器类时出现问题(不命名类型)
- 模板元程序查找相似的连续类型名称
- 可以从std::string继承以提供类型一致性吗
- 编译字符类型的时一致性哈希
- 可变/非可变模板之间函数类型衰减的不一致性
- 提供方法指针和派生类型对象时,std::bind的一致性