虚函数可以具有默认参数吗?


164

如果我声明一个基类(或接口类)并为其一个或多个参数指定默认值,那么派生类是否必须指定相同的默认值;如果没有,那么默认值将出现在派生类中吗?

附录:我也对在不同的编译器中如何处理此问题以及在这种情况下对“推荐”实践的任何投入感兴趣。


1
这似乎很容易测试。你试过了吗?
andand 2010年

22
我正在尝试中,但是我还没有找到有关行为将如何“定义”的具体信息,因此我最终将找到针对我的特定编译器的答案,但不会告诉我所有编译器是否都将执行相同的操作事情。我也对推荐的做法感兴趣。
阿诺德·斯彭斯

1
行为定义明确,我怀疑您会发现编译器将其弄错了(嗯,也许如果您测试gcc 1.x或VC ++ 1.0或类似的东西)。建议的做法是反对这样做。
杰里·科芬

Answers:


212

虚拟机可能具有默认设置。基类中的默认值不被派生类继承。

使用哪种默认值(即基类或派生类)由用于调用函数的静态类型确定。如果通过基类对象,指针或引用进行调用,则使用基类中表示的默认值。相反,如果通过派生类对象,指针或引用进行调用,则使用派生类中表示的默认值。在标准引号下有一个示例来说明这一点。

一些编译器可能会做一些不同的事情,但这就是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()
}

这是一个示例程序,演示了选择了哪些默认值。我使用的是struct这儿,而不是class简单地为了简洁ES - class并且struct是在除了默认的知名度几乎所有的方式完全一样。

#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,但没有收到编译错误“参数数量错误”!花了我一天半的时间找到错误...
steffen

2
但是有什么理由吗?为什么由静态类型决定?
user1289 2015年

2
:在虚拟方法的东西不需要的并发出有关警告锵,整洁对待默认参数github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/...
马丁Pecka

38

这是Herb Sutter的本周早期Guru帖子之一的主题。

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

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

给定

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();
}

您应该得到A :: foo1 B :: foo2 B :: foo1


7
谢谢。赫伯·萨特(Herb Sutter)的“不要那样做”具有一定的分量。
阿诺德·斯彭斯

2
@ArnoldSpence,实际上Herb Sutter超出了此建议。他认为接口根本不应该包含虚拟方法:gotw.ca/publications/mill18.htm。一旦您的方法是具体的并且不能(不应)被覆盖,就可以为它们提供默认参数了。
Mark Ransom 2013年

1
我相信他所说的“不做那个 ”是在覆盖方法中“不更改默认参数的默认值”,而不是“在虚拟方法中不指定默认参数”
Weipeng L

6

这是一个坏主意,因为您获得的默认参数将取决于对象的静态类型,而virtual分派给该函数的功能将取决于动态类型。

也就是说,当您使用默认参数调用函数时,无论该函数是否存在,默认参数都会在编译时被替换virtual

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

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

从其他答案中可以看出,这是一个复杂的主题。而不是尝试执行此操作或理解其功能(如果您现在必须询问,则维护人员将必须从现在开始一年后询问或查找它)。

而是在基类中使用默认参数创建一个公共非虚拟函数。然后,它调用私有或受保护的虚函数,该虚函数没有默认参数,并在需要时在子类中被覆盖。然后,您不必担心其工作原理的细节,并且代码非常明显。


1
一点也不复杂。将发现默认参数以及名称解析。他们遵循相同的规则。
爱德华·斯特兰奇

4

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

#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;
}

4
@GMan:[仔细看天真的]什么泄漏?:-)
杰里·科芬

我认为他指的是缺乏虚拟析构函数。但是在这种情况下,它不会泄漏。
John Dibling 2010年

1
@Jerry,如果您要通过基类指针删除派生对象,则析构函数将是虚拟的。否则,将为所有这些调用基类析构函数。这样就可以了,因为没有析构函数。:-)
chappar

2
@John:最初没有删除,这就是我所指的。我完全忽略了缺少虚拟析构函数的情况。还有... @chappar:不,这不好。它必须具有要通过基类删除的虚拟析构函数,否则您将获得未定义的行为。(此代码具有未定义的行为。)与派生类具有的数据或析构函数无关。
GManNickG

@Chappar:该代码最初并未删除任何内容。尽管它与手头的问题几乎没有关系,但我还在基类中添加了一个虚拟dtor-琐碎的dtor并不重要,但是GMan完全正确,没有它,该代码具有UB。
杰里·科芬

4

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

所以代替

//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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.