问题 C ++ 11删除重写方法


前言:

这是关于在应用于覆盖继承父虚拟方法的子类时,与C ++ 11一起引入的删除运算符的新含义的最佳实践的问题。

背景:

根据标准,引用的第一个用例是明确禁止某些类型的调用函数,否则转换将是隐含的,例如最新的§8.4.3中的示例 C ++ 11标准草案

struct sometype {
    sometype() = delete; // OK, but redundant
    some_type(std::intmax_t) = delete;
    some_type(double);
};

上面的例子清晰而有目的。但是,下面的例子中,新的运算符被覆盖并且通过将其定义为已删除而无法被调用,这让我想到了我稍后在问题部分中确定的其他场景(以下示例来自§8.4.3) C ++ 11标准草案):

struct sometype {
    void *operator new(std::size_t) = delete;
    void *operator new[](std::size_t) = delete;
};
sometype *p = new sometype; // error, deleted class operator new
sometype *q = new sometype[3]; // error, deleted class operator new[]

题:

通过将这种思想扩展到继承,我很好奇其他人关于以下用法示例是否是一个明确有效的用例,或者是否是对新添加的功能的不明确滥用的想法。请为您的答案提供理由(将提供最有说服力的推理的例子)。在下面的示例中,设计尝试通过使第二个版本的库继承自第一个版本来维护两个版本的库(需要实例化库)。这个想法是允许错误修复或对第一个库版本所做的更改自动传播到第二个库版本,同时允许第二个库版本只关注它与第一个版本的差异。要弃用第二个库版本中的函数,delete运算符用于禁止对重写函数的调用:

class LibraryVersion1 {
public:
    virtual void doSomething1() { /* does something */ }
    // many other library methods
    virtual void doSomethingN() { /* does something else */ }
};

class LibraryVersion2 : public LibraryVersion1 {
public:
    // Deprecate the doSomething1 method by disallowing it from being called
    virtual void doSomething1() override = delete;

    // Add new method definitions
    virtual void doSomethingElse() { /* does something else */ }
};

虽然我可以看到这种方法有很多好处,但我认为我更倾向于认为这是对该功能的滥用。我在上面的例子中看到的主要缺陷是经典的“is-a”继承关系被打破了。我已经阅读了很多文章,强烈建议不要使用任何继承来表达“一种即按”关系,而是使用包含函数的组合来清楚地识别类的关系。虽然下面经常讨厌的例子需要更多的努力来实现和维护(关于为这段代码编写的行数,因为每个公开可用的继承函数必须由继承类显式调用),如上所述的删除在很多方面非常相似:

class LibraryVersion1 {
public:
    virtual void doSomething1() { /* does something */ }
    virtual void doSomething2() { /* does something */ }
    // many other library methods
    virtual void doSomethingN() { /* does something */ }
};

class LibraryVersion2 : private LibraryVersion1 {
    // doSomething1 is inherited privately so other classes cannot call it
public:
    // Explicitly state which functions have not been deprecated
    using LibraryVersion1::doSomething2();
    //  ... using (many other library methods)
    using LibraryVersion1::doSomethingN();

    // Add new method definitions
    virtual void doSomethingElse() { /* does something else */ }
};

提前感谢您的回答,并进一步了解这种潜在的删除用例。


12398
2018-01-16 18:54


起源

我不认为 virtual void doSomething1() override = delete; 是合法的。你有什么 ((LibraryVersion1*)(new LibraryVersion2))->doSomething1() 做? - Andrew Tomazos
根据我的理解,即使使用强制转换为LibraryVersion1,仍然会尝试在LibraryVersion2覆盖的情况下调用已删除的函数,并导致代码无法编译。这就是我的问题中提到的“is-a”关系被破坏的地方,但它肯定会按预期强制执行弃用。 - statueuphemism
新鲜: open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3485.pdf - qPCR4vir
我不认为将虚拟函数声明为已删除的C ++ 11是合法的。 Clang和GCC都不允许这样做 - Andy Prowl
@statueuphemism:编译的代码 LibraryVersion1* 什么都不知道 LibraryVersion2那么它怎么能静态地知道(在编译时)在某个未来的日期它将被删除在某个子类中。虚拟功能是动态分派的。也许它可能抛出运行时异常,但你可以通过覆盖函数来实现 { throw DeprecatedFunctionException },所以不值得投入语言功能。 - Andrew Tomazos


答案:


C ++标准第8.4.3 / 2段 间接 禁止删除覆盖虚函数的函数:

“一个引用已删除功能的程序 隐式或明确地除了宣布之外,其形象不明确。 [注意:这个 包括隐式调用函数或明确地形成指向函数的指针或指向成员“

通过指向基类的指针调用重写虚函数是一种尝试 隐式 调用该函数。因此,根据8.4.3 / 2,允许这种情况的设计是非法的。另请注意,没有符合C ++ 11标准的编译器可以删除重写的虚函数。

更明确地说,第10.3 / 16段规定了同样的规定:

“具有删除定义(8.4)的函数不应覆盖没有删除定义的函数。同样,没有删除定义的函数不应覆盖具有已删除定义的函数。


5
2018-01-16 19:25



谢谢。我不知何故错过了那颗宝石。刚刚发现了另一个直接禁止它的引用:“具有删除定义的函数(8.4)不应该覆盖没有删除定义的函数。同样,没有删除定义的函数不应该覆盖具有删除定义的函数。删除定义。“ - statueuphemism
@statueuphemism:对,好点。让我把它整合到我的答案中 - Andy Prowl


10.3p16:

具有已删除定义(8.4)的函数不应覆盖没有已删除定义的函数。同样,没有删除定义的函数不应覆盖具有已删除定义的函数。

其他答案解释了为什么相当不错,但你有官方的Thou Shalt Not。


4
2018-01-16 20:08



哦,你已经引用了一个不同答案的评论。好吧。 - aschepler


考虑一些功能:

void f(LibraryVersion1* p)
{
    p->doSomething1();
}

这将在LibraryVersion2编写之前编译。

现在,您使用已删除的虚拟实现LibraryVersion2。

f已经编译完毕。直到运行时才知道它调用了哪个LibraryVersion1子类。

这就是为什么删除的虚拟不合法,它没有任何意义。

你能做的最好的是:

class LibraryVersion2 : public LibraryVersion1
{
public:
    virtual void doSomething1() override
    {
         throw DeletedFunctionException();
    }
}

2
2018-01-16 19:25



如果我有这样的声誉,我也会赞成这一点。关于“你能做的最好”的补充,但是在运行时抛出一个异常来表示方法的弃用对于我来说,当涉及到弃用函数时我似乎很混乱(我当然不想使用那样做的库)。我认为这种情况将要求在那时进行重新设计。但是你是对的,这可能是该语言可以强制执行已删除的重写方法定义的最佳方法。 - statueuphemism
目前尚不清楚您要解决的问题是什么。您似乎想要一种管理库版本的方法。因此,首先编写库1,然后使用库1编写应用程序1,然后编写库2,并且您希望应用程序1继续使用库2。 - Andrew Tomazos
我的例子有点做作,目前我并没有真正尝试解决问题。我认为这是一种简单维护两个软件的潜在方法,其中一个软件直接从另一个软件继承而无需任何额外的合并(类似于Python 2.7,它与Python 3.0+有一些共性,但仍在维护中)由于显着差异)。如果要维护继承自Python2_7的Python3_0类,共享代码中的错误修复将自动传播。还有其他更好的设计来实现这一目标。 - statueuphemism