【问题标题】:Can virtual functions have default parameters?虚函数可以有默认参数吗?
【发布时间】:2011-04-01 19:21:52
【问题描述】:

如果我声明一个基类(或接口类)并为其一个或多个参数指定默认值,派生类是否必须指定相同的默认值?如果不需要,哪些默认值将在派生类中体现?

附录:我也对如何在不同的编译器之间进行处理以及在这种情况下对“推荐”实践的任何输入感兴趣。

【问题讨论】:

  • 这似乎很容易测试。你试过了吗?
  • 我正在尝试它,但我还没有找到关于如何“定义”行为的具体信息,所以我最终会为我的特定编译器找到答案,但这不会说明如果所有编译器都会做同样的事情,我会这样做。我也对推荐的做法感兴趣。
  • 行为定义明确,我怀疑你会发现编译器出错(好吧,也许如果你测试 gcc 1.x 或 VC++ 1.0 或类似的东西)。建议的做法是反对这样做。

标签: c++ c++11 c++03


【解决方案1】:

虚拟可能有默认值。基类中的默认值不会被派生类继承。

使用哪个默认值 - 即基类或派生类 - 由用于调用函数的静态类型决定。如果通过基类对象、指针或引用调用,则使用基类中表示的默认值。相反,如果您通过派生类对象、指针或引用调用,则使用派生类中表示的默认值。标准引用下面有一个例子可以证明这一点。

一些编译器可能会做一些不同的事情,但这是 C++03 和 C++11 标准所说的:

8.3.6.10:

虚函数调用 (10.3) 使用 中的默认参数 虚函数声明 决定 通过表示对象的指针或引用的静态类型。一个 派生中的重写函数 类不从它的函数获取默认参数 覆盖。示例:

struct A {
  virtual void f(int a = 7);
};
struct B : public A {
  void f(int a);
};
void m()
{
  B* pb = new B;
  A* pa = pb;
  pa->f(); //OK, calls pa->B::f(7)
  pb->f(); //error: wrong number of arguments for B::f()
}

这是一个示例程序,用于演示选择了哪些默认值。我在这里使用structs 而不是classes 只是为了简洁——classstruct 在几乎所有方面都完全相同,除了默认可见性。

#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>

using std::stringstream;
using std::string;
using std::cout;
using std::endl;

struct Base { virtual string Speak(int n = 42); };
struct Der : public Base { string Speak(int n = 84); };

string Base::Speak(int n) 
{ 
    stringstream ss;
    ss << "Base " << n;
    return ss.str();
}

string Der::Speak(int n)
{
    stringstream ss;
    ss << "Der " << n;
    return ss.str();
}

int main()
{
    Base b1;
    Der d1;

    Base *pb1 = &b1, *pb2 = &d1;
    Der *pd1 = &d1;
    cout << pb1->Speak() << "\n"    // Base 42
        << pb2->Speak() << "\n"     // Der 42
        << pd1->Speak() << "\n"     // Der 84
        << endl;
}

这个程序的输出(在 MSVC10 和 GCC 4.4 上)是:

Base 42
Der 42
Der 84

【讨论】:

  • 感谢您的参考,它告诉我我可以合理预期跨编译器的行为(我希望如此)。
  • 这是对我之前总结的更正:我会接受这个答案作为参考,并提到集体建议是可以在虚函数中使用默认参数,只要它们没有更改先前在祖先类中指定的默认参数。
  • 我使用的是 gcc 4.8.1,但没有收到“参数数量错误”的编译错误!!!我花了一天半的时间才找到错误...
  • 但这有什么理由吗?为什么是由静态类型决定的?
  • Clang-tidy 将虚拟方法上的默认参数视为不需要的东西并发出警告:github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
【解决方案2】:

这是 Herb Sutter 早期 Guru of the Week 帖子之一的主题。

他在这个主题上说的第一件事就是不要那样做。

更详细地说,是的,您可以指定不同的默认参数。它们的工作方式与虚拟功能不同。对象的动态类型调用虚函数,而默认参数值基于静态类型。

给定

class A {
    virtual void foo(int i = 1) { cout << "A::foo" << i << endl; }
};
class B: public A {
    virtual void foo(int i = 2) { cout << "B::foo" << i << endl; }
};
void test() {
A a;
B b;
A* ap = &b;
a.foo();
b.foo();
ap->foo();
}

你应该得到 答::foo1 B::foo2 B::foo1

【讨论】:

  • 谢谢。 Herb Sutter 的一句“不要那样做”很有分量。
  • @ArnoldSpence,事实上 Herb Sutter 超出了这个建议。他认为接口根本不应该包含虚方法:gotw.ca/publications/mill18.htm。一旦你的方法是具体的并且不能(不应该)被覆盖,给它们提供默认参数是安全的。
  • 我相信他所说的“不要做 that”的意思是“不要更改默认参数的默认值”在覆盖方法中,而不是“不要” t 在虚拟方法中指定默认参数"
【解决方案3】:

这是个坏主意,因为您获得的默认参数将取决于对象的 static 类型,而调度到的 virtual 函数将取决于 dynamic em> 输入。

也就是说,当你调用一个带有默认参数的函数时,无论该函数是否为virtual,都会在编译时替换默认参数。

@cppcoder 在他的 [已关闭] question 中提供了以下示例:

struct A {
    virtual void display(int i = 5) { std::cout << "Base::" << i << "\n"; }
};
struct B : public A {
    virtual void display(int i = 9) override { std::cout << "Derived::" << i << "\n"; }
};

int main()
{
    A * a = new B();
    a->display();

    A* aa = new A();
    aa->display();

    B* bb = new B();
    bb->display();
}

产生以下输出:

Derived::5
Base::5
Derived::9

借助上面的解释,很容易看出原因。在编译时,编译器从指针的静态类型的成员函数中替换默认参数,使main 函数等效于以下内容:

    A * a = new B();
    a->display(5);

    A* aa = new A();
    aa->display(5);

    B* bb = new B();
    bb->display(9);

【讨论】:

    【解决方案4】:

    正如其他答案所详述的那样,这是个坏主意。但是由于没有人提到简单有效的解决方案,这里是:将参数转换为结构,然后您可以为结构成员设置默认值!

    所以不是,

    //bad idea
    virtual method1(int x = 0, int y = 0, int z = 0)
    

    这样做,

    //good idea
    struct Param1 {
      int x = 0, y = 0, z = 0;
    };
    virtual method1(const Param1& p)
    

    【讨论】:

      【解决方案5】:

      正如您从其他答案中看到的那样,这是一个复杂的主题。而不是尝试这样做或了解它的作用(如果您现在必须询问,维护人员将不得不在一年后询问或查找它)。

      相反,使用默认参数在基类中创建一个公共非虚拟函数。然后它调用一个没有默认参数的私有或受保护的虚函数,并根据需要在子类中被覆盖。那么你就不必担心它是如何工作的细节,而且代码非常明显。

      【讨论】:

      • 一点也不复杂。默认参数与名称解析一起被发现。它们遵循相同的规则。
      【解决方案6】:

      这是一个你可以通过测试很好地弄清楚的部分(即,它是语言的一个足够主流的部分,大多数编译器几乎肯定能做到这一点,除非你看到编译器之间的差异,否则它们的输出可以被认为是相当不错的权威)。

      #include <iostream>
      
      struct base { 
          virtual void x(int a=0) { std::cout << a; }
          virtual ~base() {}
      };
      
      struct derived1 : base { 
          void x(int a) { std:: cout << a; }
      };
      
      struct derived2 : base { 
          void x(int a = 1) { std::cout << a; }
      };
      
      int main() { 
          base *b[3];
          b[0] = new base;
          b[1] = new derived1;
          b[2] = new derived2;
      
          for (int i=0; i<3; i++) {
              b[i]->x();
              delete b[i];
          }
      
          derived1 d;
          // d.x();       // won't compile.
          derived2 d2;
          d2.x();
          return 0;
      }
      

      【讨论】:

      • @GMan:[仔细地看着无辜]什么泄漏? :-)
      • 我认为他指的是缺少虚拟析构函数。但在这种情况下它不会泄漏。
      • @Jerry,如果您通过基类指针删除派生对象,则析构函数是虚拟的。否则将为所有这些调用基类析构函数。在这没关系,因为没有析构函数。 :-)
      • @John:最初没有删除,这就是我所指的。我完全忽略了缺少虚拟析构函数。而且...... @chappar:不,这不好。它必须有一个要通过基类删除的虚拟析构函数,否则你会得到未定义的行为。 (此代码具有未定义的行为。)它与派生类的数据或析构函数无关。
      • @Chappar:代码最初并没有删除任何内容。虽然它与手头的问题几乎无关,但我还在基类中添加了一个虚拟 dtor——用一个微不足道的 dtor,它几乎不重要,但 GMan 是完全正确的,没有它,代码就有 UB。
      猜你喜欢
      • 2011-04-18
      • 2014-04-28
      • 2015-05-19
      • 1970-01-01
      • 2018-08-05
      • 2012-07-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多