禁止指针重新赋值

8

我正在阅读《Effective C++ 第三版》(作者为Scott Meyers)。

他说通常不建议继承一个不包含虚函数的类,因为如果你以某种方式将派生类的指针转换为基类的指针,然后使用 delete 删除它可能会导致未定义的行为。

这是他提供的(人工制造的)例子:

class SpecialString: public std::string{
   // ...
}

SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
ps = pss;
delete ps;    // undefined! SpecialString destructor won't be called

我理解为什么会出现错误,但是在 SpecialString 类中是否有任何方法可以防止像 ps = pss 这样的情况发生?

Meyers 指出(在书的不同部分)一种常见的技术是,声明一个特定的函数但故意不定义它来明确禁止某些行为在类中被允许。他给出了一个例子:复制构造。例如,对于不想允许复制构造的类,声明一个私有复制构造函数但不定义它,这样任何尝试使用它的操作都会导致编译时错误。

我知道这个例子中的 ps = pss 不是复制构造,只是想知道是否有其他方法可以明确地防止这种情况发生(除了 "不要那样做" 的答案)。


1
也许这个链接能帮到你? - lakeweb
1
由于您无法重载指针赋值运算符,因此我认为您可以使 SpecialString 禁止公共访问其 new 运算符,并要求使用工厂函数来返回一个自定义的智能指针类型,该类型不会暴露原始指针值。 - Dai
3
@Mike,你无法重载=来进行指针赋值,因为指针是标量。 - Dai
当您扩展std::string时,您实际上签署的合同的一部分是SpecialString [isa](https://en.wikipedia.org/wiki/Is-a)字符串,并且`ps = pss;`是完全合法的。有人可能会使用一些巫术或模板魔法来限制此操作,但结果可能会比“不要这样做”更加混乱。 - user4581301
@JaMiT 是的,但也许 unique_ptr<T, default_deleter<T>> 应该禁用那种转换... - aschepler
显示剩余7条评论
3个回答

2
该语言允许从派生类指针隐式转换为基类指针,只要基类是可访问的且不含歧义。这不是用户代码可以覆盖的。此外,如果基类允许销毁,那么一旦将指向派生类的指针转换为指向基类的指针,您就可以通过该指针删除基类,导致未定义行为。这也不能被派生类覆盖。
因此,您不应从未设计为基类的类派生。您的书中缺乏解决方法表明缺乏解决方法。
上述内容中有两个点可能值得再仔细看一下。首先:"只要基类可访问且不模糊"。(我不想讨论"模糊"的问题。)您可以通过将基类设置为private来防止在类实现之外的代码中将指向派生类的指针转换为指向基类的指针。 如果这样做,您应该花些时间思考为什么要进行继承。私有继承通常很少使用。通常,不从其他类派生而是拥有一个数据成员,其类型是其他类,可能更有意义(或者至少与继承同样有意义)。
第二点:"如果基类允许销毁"。这不适用于您的示例,因为您无法更改基类定义,但它适用于声明"通常继承不包含虚拟[析构函数]的类不是一个好主意"。还有另一个可行的选择。如果该类的析构函数是protected,那么从没有虚拟函数的类继承可能是合理的。如果一个类的析构函数是受保护的,那么你不能在指向该类的指针上使用delete(除了该类和派生类的实现)。因此,只要基类具有虚拟析构函数或受保护的析构函数,就可以避免未定义的行为。

关于protected析构函数的有趣点-我没有想到这一点。但是,阅读了本章的其余部分后,迈耶斯指出,有些类根本就不是设计成作为基类使用的,因此您不应尝试这样做。他还评论说,不幸的是C ++没有任何机制来防止这些类型的类继承(例如类似于C#中的“sealed”类)。虽然我想这本书有点过时了,因为“final”现在是一个可以实现这一点的关键字。谢谢您的帮助。 - ImaginaryHuman072889

1

有两种方法可能是有意义的:

  1. 如果真正的问题是 string 并不打算从中派生,并且您对其具有控制权-那么您可以使其成为 final。(显然,您无法对 std::string 进行此操作,因为您无法控制 std::string

  2. 如果 string 可以被派生,但不能进行多态使用,则可以从 SpecialString 中删除 newdelete 函数,以防止通过 new 分配一个。

例如:

#include <string>

class SpecialString : std::string {
  void* operator new(size_t size)=delete;
};

int main() {
  SpecialString ok;
  SpecialString* not_ok = new SpecialString();
}

无法编译,报错如下:
code.cpp:9:27: error: call to deleted function 'operator new'
  SpecialString* not_ok = new SpecialString();
                          ^
code.cpp:4:9: note: candidate function has been explicitly deleted
  void* operator new(size_t size)=delete;

请注意,这并不能阻止奇怪的行为,例如:

SpecialString ok;
std::string * ok_p = &ok;
ok_p->something();

如果你提供了一个,它将始终调用std::string::something而不是SpecialString::something。这可能不是你期望的结果。

除非使用 ::new SpecialString(),否则仍会存在这个问题。 - aschepler
1
使用::new进行除了实现T::operator new之外的任何操作的人,都应该承担他们所遭受到的任何困难和未定义行为。 - Ben Voigt

0

如果你能预防错误,就不必在运行时检查它。

正如你的书所说,在基类中使用虚析构函数(带实现),并且当然还要为派生类定义一个析构函数,这样如果你将指向派生类的指针分配给基类,它首先作为基类被销毁,然后作为派生对象被销毁。

你可以在这里看到一个例子 https://www.geeksforgeeks.org/virtual-destructor

有关更具体的澄清,请参见评论


问题是如何在派生类代码中防止这种行为。当然,如果您可以访问基类,那么这个问题就不复存在 - 只需使基类析构函数虚拟即可。但是,并不总是能够修改基类的情况(例如,如果您的基类是std::string,如我所问)。 - ImaginaryHuman072889
好的,我没有考虑到您指的是从您无法访问的类派生...现在在编译时禁止赋值有点违背了多态和重载的概念,可能会强制您切割对象,这是您最不想做的事情。因此,我想除了小心谨慎外,没有什么可做的了,或者您可以实现自己的基类。 - prophet-five

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