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

Arn*_*nce 152 c++ c++11 c++03

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

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

Joh*_*ing 196

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

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

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

(编辑: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()
}
Run Code Online (Sandbox Code Playgroud)
—end example]
Run Code Online (Sandbox Code Playgroud)

编辑这是一个示例程序,用于演示拾取的默认值.我使用的是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;
}
Run Code Online (Sandbox Code Playgroud)

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

Base 42
Der 42
Der 84
Run Code Online (Sandbox Code Playgroud)

  • Clang-tidy 将虚拟方法的默认参数视为不需要的东西并发出警告:https://github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/google/DefaultArgumentsCheck.cpp (4认同)
  • +1这是一个不依赖编译器的好提示:) (3认同)
  • 这是对我之前总结的更正:我将接受这个答案作为参考,并提到集体建议是,在虚拟函数中具有默认参数是可以的,只要它们不更改先前在祖先中指定的默认参数即可班级。 (2认同)
  • 但是有什么理由吗?为什么由静态类型决定? (2认同)

Dav*_*ley 36

这是Herb Sutter早期的本周大师之一的话题.

他在这个问题上所说的第一件事是不要那样做.

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

特定

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();
}
Run Code Online (Sandbox Code Playgroud)

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

  • 谢谢.Herb Sutter的"不要那样做"带来了一些重量. (7认同)
  • 我相信他所说的“不要做*那个*”是在覆盖方法中“不要更改默认参数的默认值”,而不是“不要在虚拟方法中指定默认参数” (4认同)
  • @ArnoldSpence,事实上Herb Sutter超越了这个建议.他认为界面根本不应包含虚拟方法:http://www.gotw.ca/publications/mill18.htm.一旦你的方法具体并且不能(不应该)被覆盖,给它们默认参数是安全的. (2认同)

Shi*_*hah 7

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

所以,而不是,

//bad idea
virtual method1(int x = 0, int y = 0, int z = 0)
Run Code Online (Sandbox Code Playgroud)

做这个,

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


Mar*_*k B 6

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

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


Okt*_*ist 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();
}
Run Code Online (Sandbox Code Playgroud)

产生以下输出:

Derived::5
Base::5
Derived::9
Run Code Online (Sandbox Code Playgroud)

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

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

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

    B* bb = new B();
    bb->display(9);
Run Code Online (Sandbox Code Playgroud)


Jer*_*fin 5

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

  • @GMan:[小心地看着无辜]什么泄漏?:-) (4认同)
  • @John:最初没有删除,这就是我所指的。我完全忽略了虚拟析构函数的缺乏。而且... @chappar:不,这不行。它*必须*具有要通过基类删除的虚拟析构函数,否则您会得到未定义的行为。(此代码具有未定义的行为。)它与派生类具有的数据或析构函数无关。 (2认同)