能否防止在编译时通过父类调用继承的私有成员?

Can you prevent inherited private members being called through the parent at compile time?

本文关键字:继承 调用 成员 父类 编译      更新时间:2023-10-16

如果你有一个功能丰富的类,可能是一个你不拥有/控制的类,通常情况下,你想添加一些功能,所以派生是有意义的。

有时候你也想做减法,那就是禁止基本接口的某些部分。我所见过的常见习惯用法是派生并使某些成员函数为私有,然后不实现它们。如下:

class Base
{
public:
  virtual void foo() {}
  void goo() { this->foo(); }
};
class Derived : public Base
{
private:
   void foo();
};

别的地方:

Base * b= new Derived;

和另一个位置:

b->foo();  // Any way to prevent this at compile time?
b->goo();  // or this?

似乎如果编译不知道它是派生的,你能做的最好的事情就是不实现,让它在运行时失败。

问题出现在当你有一个库,你不能改变,它接受一个指向基的指针,你可以实现一些方法,但不是全部。因此,部分库是有用的,但如果在编译时不知道哪些函数将调用哪些函数,则可能会冒核心转储的风险。

使之更加困难的是,其他人可能继承了你的类并想要使用这个库,他们可能会添加一些你没有添加的函数。

还有别的办法吗?在c++中11吗?在c++中14 ?

让我们分析一下,主要集中在两个方面:

class Base
{
public:
    virtual void foo() {} // This 1)
// ... 
class Derived : public Base // and this 2)

在1)中,您告诉世界Base的每个对象都公开提供foo()方法。这意味着当我有Base*b时,我可以调用b->foo() -和b->goo()

在2)中,你告诉世界你的类Derived公开表现得像Base。因此,以下是可能的:

void call(Base *b) { b->foo(); }
int main() {
    Derived *b = new Derived();
    call(b);
    delete b;
}

希望你看到call(Base*)无法知道b是否是派生的,因此它不可能在编译时决定调用foo是否合法。


有两种处理方法:

  • 你可以改变foo()的可见性。这可能不是您想要的,因为其他类可以从Base派生,而有人终究想调用foo。请记住,虚拟方法可以是私有的,因此您可能应该将Base声明为
class Base
{
  virtual void foo() {}
public:
  void goo() { this->foo(); }
};
  • 您可以更改Derived,使其从Base继承protectedprivate。这意味着没有人/只有继承类才能"看到"DerivedBase,并且不允许调用foo()/goo():
class Derived : private Base
{
private:
    void foo() override;
    // Friends of this class can see the Base aspect
// .... OR
// public: // this way
    // void foo(); // would allow access to foo()
};
// Derived d; d.goo() // <-- illegal
// d.foo() // <-- illegal because `private Base` is invisible

您通常应该选择后者,因为它不涉及更改Base类的接口-"真正的"实用程序。


TL;DR:派生类是提供至少该接口的契约。不可能

这似乎是你想要做的:

struct Library {
    int balance();
    virtual int giveth(); // overrideable
    int taketh(); // part of the library
};
/* compiled into the library's object code: */
int Library::balance() { return giveth() - taketh(); }
/* Back in header files */
// PSEUDO CODE
struct IHaveABadFeelingAboutThis : public Library {
    int giveth() override; // my implementation of this
    int taketh() = delete; // NO TAKE!
};

所以你不能在IHaveABadFeelingAboutThis上调用taketh(),即使它被强制转换为基类。

int main() {
    IHaveABadFeelingAboutThis x;
    Library* lib = &x;
    lib->taketh(); // Compile error: NO TAKE CANDLE!
    // but how should this be handled?
    lib->balance();
}

如果你想呈现一个与底层库不同的接口,你需要一个facade来呈现你的接口,而不是库的接口。

class Facade {
    struct LibraryImpl : public Library {
        int giveth() override;
    };
    LibraryImpl m_impl;
public:
    int balance() { return m_impl.balance(); }
    virtual int giveth() { return m_impl.giveth(); }
    // don't declare taketh
};
int main() {
    Facade f;
    int g = f.giveth();
    int t = f.taketh(); // compile error: undefined
}

虽然我不认为你的整体情况是好的设计,我也在评论中分享了很多观点,但我也能理解你不控制的代码是如何参与的。我不相信有任何编译时解决方案可以很好地定义您的问题,但是比起使方法私有而不实现它们,更可取的是实现整个接口,并简单地使任何无法处理的方法抛出异常。这样至少定义了行为,如果您认为可以从需要您无法提供的接口的库函数中恢复,您甚至可以使用try/catch。

如果您有class A:public B,那么您应该遵循https://en.wikipedia.org/wiki/Liskov_substitution_principle

Liskov替换原则是指向a的指针在任何情况下都可以用作指向b的指针。B有什么要求,A也应该满足。

这很难实现,这也是为什么许多人认为oo风格的继承远没有看起来那么有用的原因之一。

您的base暴露了virtual void foo()。通常的契约意味着这样的foo可以被调用,如果满足它的前提条件,它将返回。

如果从base派生,则不能加强前置条件,也不能放松后置条件。

另一方面,如果记录了base::foo()(并且支持base的消费者),那么它抛出错误的可能性(例如method_does_not_exist),那么您可以派生并让您的实现抛出该错误。请注意,即使合约说它可以这样做,在实践中,如果没有测试,消费者可能无法工作。

违反Liskov替代原则是产生大量bug和不可维护代码的好方法。只有当你真的、真的需要的时候才去做