【问题标题】:Is it ever "moral" to override a nonvirtual function? [closed]覆盖非虚拟功能是否“道德”? [关闭]
【发布时间】:2011-03-13 13:50:29
【问题描述】:

我长期使用以下 C++ 经验法则:

如果一个类重写了它的一个函数 基类,函数应该是 在基中声明virtual

我想我遇到了这条规则的一个例外。为了判断这是否合理,或者指出我的设计存在缺陷,我提出了这个问题。我想获得示例或更好的规则。


编辑:我尝试在这里描述我的用例,我明白我真的不需要继承!

不过,我想问一个一般性问题。感谢您的回答!

【问题讨论】:

  • 恕我直言,我认为如果有理由“隐藏”,并且您知道自己在做什么,为什么不呢?
  • 你为什么不告诉我们你为什么认为你的案例是一个例外?然后我们可以说是否同意,如果不同意,为什么不同意。在那之前,这真的不是问题。
  • “士气”是什么意思?在某些情况下,我不会按照你的意愿工作。
  • @Zimbabao,目前“士气”很低,我们不确定是否隐藏......(这与“道德”有点不同 - @jalf 描述嗯... ;) )
  • @David 很明显提问者的意思是“隐藏”。我怀疑他是否读过标准,因为他是初学者。关闭一个基于不是 100% 正确的术语或使用英语的问题,我想我们需要在这里关闭所有问题的 90%。我不想结束这个问题,我想请@anatolyg 给出一个简短的代码示例。

标签: c++ virtual-functions


【解决方案1】:

您不能覆盖非虚拟函数。您唯一能做的就是隐藏基类实现。但这并没有为您提供虚函数提供的多态行为。

【讨论】:

    【解决方案2】:

    我个人不喜欢这个,但有时它很有用。标准库也使用它:

    stringstream ss;
    
    /* Imagine you want to redirect all output that goes into "ss"
     * to "cout". The following does NOT work! */
    ss.rdbuf(cout.rdbuf());
    

    为什么它不起作用?因为stringstream 隐藏了ios::rdbuf 与一个相同的命名函数,该函数只提供对其内部std::stringbuf 的读取访问,而不是对附加缓冲区的读取访问。您需要执行以下操作

    ss.std::ios::rdbuf(cout.rdbuf());
    

    现在,附加到流的缓冲区不等于ss.rdbuf() 返回的缓冲区。不过我个人不喜欢这种设计。


    不过,我曾经很好地利用了隐藏。在我看来,隐藏需要一个要求:

    • 行为应该相同。

    在我的例子中,我有一个像这样的基类(不是很接近,但它传达了这种情况)。

    template<typename T>
    struct A {
      void doSomething() {
        T t;
        t.doIt();
      }
    };
    
    class Foo;
    struct B : A<Foo> {
    
    };
    
    B b;
    

    拨打b.doSomething()会发生什么?它需要Foo 的标头,因为函数要在类上调用doIt 并创建该类型的变量。解决方法很简单

    class Foo;
    struct B : A<Foo> {
      void doSomething();
    };
    
    // and in the .cpp file:
    #include "Foo.h"
    
    void B::doSomething() {
      A<Foo>::doSomething();
    }
    

    这样,我防止了B 类的每个用户都需要包含 Foo 的标头。只有知道它依赖于“Foo”的 B 的 cpp 文件必须这样做。

    【讨论】:

    • +1 我不知道您可以使用 derived.Base::method(args...) 从外部调用基类中的方法。
    【解决方案3】:

    我认为你记错了规则。规则是:“如果您要从基类重写虚拟方法,则应将重写方法声明为虚拟方法。”

    这是一个代码风格规则,可以防止混淆,因为 virtual 修饰符是继承的。

    【讨论】:

    • +1 我猜这就是 OP 的真正意思。
    • 不,我不打算询问编码风格的规则;我的意思是设计规则。
    【解决方案4】:

    有可能吗?是的。 是道德的吗?这取决于你对道德的定义。 这会让你的开发人员和未来的维护程序员感到困惑吗?绝对是的!

    派生类中的函数与基类中的非虚函数同名,只是隐藏了基类函数及其重载。

    我认为这是对继承的滥用,因为您基本上是在说您想以基类合同不允许您这样做的方式重新定义某事的完成方式。通过在基类中使函数成为非虚拟函数,您指定了希望函数执行的操作(其接口),更重要的是如何您希望它执行此操作(其实现)。使函数成为非虚拟函数的含义是,它的接口和实现都不应该在派生类中更改。

    【讨论】:

      【解决方案5】:

      你可以做到。但可能这不是你想要的。 OOP 的主要原则之一是运行时多态性。在这种情况下,您可能无法使用它。

      检查以下代码。它试图使用基类型对象来处理超类型对象。但是如果是非虚拟的,它就不起作用了。

      我的预期输出是

      In B::printNonV() 
      In B::printV()
      

      但我得到了

      In A::printNonV() 
      In B::printV()
      

      .

      #include <iostream>
      
      using  namespace std;
      
      class A
      {
      
          public:
          void printNonV(){
            cout<<"In A::printNonV() "<<endl;
          }
      
          virtual void printV(){
            cout<<"In A::printV()"<<endl;
         }
      };
      
      
      class B:public A
      {
      
          public:
          void printNonV(){
            cout<<"In B::printNonV()"<<endl;
          }
      
          virtual void printV(){
            cout<<"In B::printV()"<<endl;
         }
      };
      
      int main(){
        A* b=new B();
      
        b->printNonV();
        b->printV();
      }
      

      【讨论】:

      • 好点。换句话说,当方法被覆盖时,总是使用 virtual 并不是一个规则。当您想要这种特定行为(大多数情况)时,使用 virtual 是一条规则。如果覆盖非虚拟方法没有用,编译器将报告为错误。我仍然想不出覆盖非虚拟方法有用的情况,但可能会有。
      【解决方案6】:

      从基类重载(而不是覆盖)非虚拟函数的一个示例是使用 CRTP 实现模拟动态绑定:

      // in the shared header
      template <typename Derived>
      struct GenericOSDetails {
          size_t preferred_character_size() {
              return 1; // we expect `char` to be the preferred character type
          }
          size_t preferred_string_length(size_t numchars) {
              return numchars * static_cast<Derived&>(*this).preferred_character_size();
          }
          // other functions that do considerably more useful things based on
          // the preferred character size and encoding.
      };
      
      // in the linux header
      struct LinuxOSDetails : GenericOSDetails<LinuxOSDetails> {
          // we're happy with the defaults.
      };
      
      // in the windows header
      struct WindowsOSDetails : GenericOSDetails<WindowsOSDetails> {
          // configure ourselves for "Unicode" vs non-Unicode builds.
          size_t preferred_character_size() {
              return sizeof(TCHAR);
          }
      };
      

      注意模拟动态绑定——使用这种技术,WindowsOSDetails 的实例将不会作为指向基类GenericOSDetails&lt;WindowsOSDetails&gt; 的指针传递,因此不需要虚函数。始终使用静态绑定,但基类仍然可以调用派生类函数并获得重载版本。

      说实话,我不确定这有多大的实用性。可能 99% 的时间你可能会想到使用它,它要么是过早的优化,要么你应该提供一个策略作为模板参数,以及一个用于常见情况的默认策略,并且根本不使用继承。但是另外 1% 的时间,一般来说,当你想使用继承但又不想或不需要动态多态性的任何时候,如果你愿意,你可以避免使用虚函数。

      AFAIK 只有在模板繁重的代码中,您才会对不依赖动态多态性的继承做任何特别有趣的事情。你普通的 OOP 范式不感兴趣。

      【讨论】:

        【解决方案7】:

        我认为人们普遍认为隐藏基类功能不是一件好事,如果有的话,应该很少这样做。更令人惊讶的问题之一是您确实破坏了多态行为(如Asha 所述)。以下是为什么这往往令人惊讶的示例。

        struct Person {
            virtual std::string get_name() const = 0;
            void print_name(std::ostream& s) const { s << get_name() << std::endl; }
        };
        struct Fred: Person {
            virtual std::string get_name() const { return "Fred"; }
        };
        struct Barney: Person {
            virtual std::string get_name() const { return "Barney"; }
            void print_name(std::ostream& s) const { s << "Bam Bam" << std::endl; }
        };
        std::ostream& operator<<(std::ostream& s, Person const& p) {
            p.print_name(s);
            return s;
        }
        
        int main() {
            Fred fred;
            Barney barney;
            barney.print_name(std::cout);
            std::cout << fred << barney << std::endl;
            return 0;
        }
        

        这个输出:

        Bam Bam
        Fred
        Barney
        

        隐藏基类会破坏Liskov Substitution Principle,这会以各种令人不快的方式让实现感到惊讶。

        【讨论】:

        • 但是拥有一个基类可以是一个实现细节,并不一定意味着这些类是多态的。然后就可以了。
        • @BoPersson - 是的。但是您会使用privateprotected 继承(在C++ 中)或在Java 中扩展一个package private 类。如果继承是可见的,那么它就不是实现细节,因为有人可以(并且将会)依赖它。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-10-30
        • 2012-10-26
        • 2012-06-19
        • 2014-05-12
        • 2020-04-01
        • 1970-01-01
        • 2011-05-08
        相关资源
        最近更新 更多