Các chức năng ảo có thể có các tham số mặc định?


164

Nếu tôi khai báo một lớp cơ sở (hoặc lớp giao diện) và chỉ định một giá trị mặc định cho một hoặc nhiều tham số của nó, thì các lớp dẫn xuất phải chỉ định cùng một mặc định và nếu không, mặc định nào sẽ biểu hiện trong các lớp dẫn xuất?

Phụ lục: Tôi cũng quan tâm đến cách xử lý việc này trên các trình biên dịch khác nhau và bất kỳ đầu vào nào về thực hành "được đề xuất" trong kịch bản này.


1
Đây có vẻ là một điều dễ dàng để kiểm tra. Bạn đã thử chưa?
andand

22
Tôi đang trong quá trình dùng thử nhưng tôi chưa tìm thấy thông tin cụ thể về cách "xác định" hành vi sẽ như thế nào nên cuối cùng tôi sẽ tìm thấy câu trả lời cho trình biên dịch cụ thể của mình nhưng điều đó sẽ không cho tôi biết nếu tất cả các trình biên dịch sẽ làm như vậy Điều. Tôi cũng quan tâm đến thực hành được đề nghị.
Arnold Spence

1
Hành vi được xác định rõ và tôi nghi ngờ bạn sẽ tìm thấy trình biên dịch bị lỗi (vâng, có thể nếu bạn kiểm tra gcc 1.x hoặc VC ++ 1.0 hoặc đại loại như thế). Khuyến nghị thực hành là chống lại việc này.
Jerry Coffin

Câu trả lời:


212

Ảo có thể có mặc định. Mặc định trong lớp cơ sở không được kế thừa bởi các lớp dẫn xuất.

Mặc định nào được sử dụng - tức là lớp cơ sở 'hoặc lớp dẫn xuất' - được xác định bởi loại tĩnh được sử dụng để thực hiện cuộc gọi đến hàm. Nếu bạn gọi thông qua một đối tượng lớp cơ sở, con trỏ hoặc tham chiếu, mặc định được biểu thị trong lớp cơ sở được sử dụng. Ngược lại, nếu bạn gọi qua một đối tượng lớp dẫn xuất, con trỏ hoặc tham chiếu mặc định được biểu thị trong lớp dẫn xuất được sử dụng. Có một ví dụ dưới đây Báo giá tiêu chuẩn chứng minh điều này.

Một số trình biên dịch có thể làm một cái gì đó khác nhau, nhưng đây là những gì mà Tiêu chuẩn C ++ 03 và C ++ 11 nói:

8.3.6.10:

Một cuộc gọi hàm ảo (10.3) sử dụng các đối số mặc định trong khai báo hàm ảo được xác định bởi kiểu tĩnh của con trỏ hoặc tham chiếu biểu thị đối tượng. Hàm ghi đè trong lớp dẫn xuất không thu được các đối số mặc định từ hàm mà nó ghi đè. Thí dụ:

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

Dưới đây là một chương trình mẫu để chứng minh những gì mặc định được chọn. Tôi đang sử dụng structs ở đây chứ không classchỉ đơn giản là vì đơn giản - classstructhoàn toàn giống nhau về mọi mặt ngoại trừ khả năng hiển thị mặc định.

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

Đầu ra của chương trình này (trên MSVC10 và GCC 4.4) là:

Base 42
Der 42
Der 84

Cảm ơn đã tham khảo, điều đó cho tôi biết hành vi mà tôi có thể mong đợi một cách hợp lý trên các trình biên dịch (tôi hy vọng).
Arnold Spence

Đây là một sửa chữa cho bản tóm tắt trước đây của tôi: Tôi sẽ chấp nhận câu trả lời này để tham khảo và đề cập rằng khuyến nghị tập thể là không có các tham số mặc định trong các chức năng ảo miễn là chúng không thay đổi các tham số mặc định được chỉ định trước đó trong tổ tiên lớp học.
Arnold Spence

Tôi đang sử dụng gcc 4.8.1 và tôi không gặp phải lỗi biên dịch "sai số lượng đối số" !!! Mất một ngày rưỡi để tìm ra lỗi ...
steffen

2
Nhưng có lý do nào cho điều đó? Tại sao nó được xác định bởi loại tĩnh?
dùng1289

2
Clang-tidy coi các tham số mặc định trên các phương thức ảo là một thứ không mong muốn và đưa ra cảnh báo về điều đó: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/ trộm
Martin Pecka

38

Đây là chủ đề của một trong những đầu Herb Sutter của Guru của Tuần bài đăng nào.

Điều đầu tiên anh nói về chủ đề này là KHÔNG LÀM ĐƯỢC.

Chi tiết hơn, có, bạn có thể chỉ định các tham số mặc định khác nhau. Chúng sẽ không hoạt động giống như các chức năng ảo. Một hàm ảo được gọi trên kiểu động của đối tượng, trong khi các giá trị tham số mặc định dựa trên kiểu tĩnh.

Được

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

bạn sẽ nhận được A :: foo1 B :: foo2 B :: foo1


7
Cảm ơn. Một "Đừng làm vậy" từ Herb Sutter mang một chút trọng lượng.
Arnold Spence

2
@ArnoldSpence, trên thực tế Herb Sutter vượt xa khuyến nghị này. Ông tin rằng một giao diện không nên chứa các phương thức ảo: gotw.ca/publications/mill18.htm . Khi các phương thức của bạn cụ thể và không thể (không nên) bị ghi đè, việc cung cấp cho chúng các tham số mặc định là an toàn.
Đánh dấu tiền chuộc

1
Tôi tin rằng ý của anh ấy là "không làm điều đó " là "không thay đổi giá trị mặc định của tham số mặc định" trong các phương thức ghi đè, không "không chỉ định tham số mặc định trong phương thức ảo"
Weipeng L

6

Đây là một ý tưởng tồi, bởi vì các đối số mặc định bạn nhận được sẽ phụ thuộc vào kiểu tĩnh của đối tượng, trong khi virtualchức năng được gửi đến sẽ phụ thuộc vào loại động .

Điều đó có nghĩa là, khi bạn gọi một hàm với các đối số mặc định, các đối số mặc định được thay thế vào thời gian biên dịch, bất kể hàm đó có virtualhay không.

@cppcoder đã đưa ra ví dụ sau trong câu hỏi [đã đóng] của mình :

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

Mà tạo ra đầu ra sau đây:

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

Với sự trợ giúp của lời giải thích ở trên, thật dễ dàng để biết lý do tại sao. Tại thời gian biên dịch, trình biên dịch thay thế các đối số mặc định từ các hàm thành viên của các kiểu tĩnh của con trỏ, làm cho mainhàm tương đương như sau:

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

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

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

4

Như bạn có thể thấy từ các câu trả lời khác, đây là một chủ đề phức tạp. Thay vì cố gắng làm điều này hoặc hiểu những gì nó làm (nếu bạn phải hỏi bây giờ, người bảo trì sẽ phải hỏi hoặc tìm kiếm nó một năm kể từ bây giờ).

Thay vào đó, hãy tạo một hàm không ảo công khai trong lớp cơ sở với các tham số mặc định. Sau đó, nó gọi một hàm ảo riêng tư hoặc được bảo vệ không có tham số mặc định và bị ghi đè trong các lớp con khi cần. Sau đó, bạn không phải lo lắng về các chi tiết về cách thức hoạt động của nó và mã rất rõ ràng.


1
Nó hoàn toàn không phức tạp. Các tham số mặc định được phát hiện cùng với độ phân giải tên. Họ theo cùng một quy tắc.
Edward Strange

4

Đây là một thứ mà bạn có thể có thể tìm ra một cách hợp lý bằng cách thử nghiệm (nghĩa là nó là một phần chính của ngôn ngữ mà hầu hết các trình biên dịch gần như chắc chắn hiểu đúng và trừ khi bạn thấy sự khác biệt giữa các trình biên dịch, đầu ra của chúng có thể được coi là có thẩm quyền khá tốt).

#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: [Cẩn thận nhìn vô tội] Rò rỉ cái gì? :-)
Jerry Coffin

Tôi nghĩ rằng anh ta đang đề cập đến việc thiếu một kẻ hủy diệt ảo. Nhưng trong trường hợp này nó sẽ không bị rò rỉ.
John Dibling

1
@Jerry, hàm hủy là ảo nếu bạn đang xóa đối tượng dẫn xuất mặc dù con trỏ lớp cơ sở. Nếu không, hàm hủy lớp cơ sở sẽ được gọi cho tất cả chúng. Trong đó là ok vì không có hàm hủy. :-)
chappar

2
@ John: Ban đầu không có xóa, đó là những gì tôi đã đề cập đến. Tôi hoàn toàn bỏ qua việc thiếu một kẻ hủy diệt ảo. Và ... @chappar: Không, không ổn. Nó phải có một hàm hủy ảo để được xóa thông qua một lớp cơ sở hoặc bạn có hành vi không xác định. (Mã này có hành vi không xác định.) Nó không liên quan gì đến dữ liệu hoặc hàm hủy mà các lớp dẫn xuất có.
GManNickG

@Chappar: Mã ban đầu không xóa bất cứ thứ gì. Mặc dù hầu như không liên quan đến câu hỏi trong tay, tôi cũng đã thêm một dtor ảo vào lớp cơ sở - với một dtor tầm thường, nó hiếm khi quan trọng, nhưng GMan hoàn toàn chính xác rằng nếu không có nó, mã có UB.
Jerry Coffin

4

Như các câu trả lời khác đã chi tiết, ý tưởng tồi của nó. Tuy nhiên vì không ai đề cập đến giải pháp đơn giản và hiệu quả, nên đây là: Chuyển đổi các tham số của bạn thành struct và sau đó bạn có thể có các giá trị mặc định cho các thành viên struct!

Vì vậy, thay vì,

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

làm cái này,

//good idea
struct Param1 {
  int x = 0, y = 0, z = 0;
};
virtual method1(const Param1& p)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.