ฟังก์ชั่นเสมือนจริงสามารถมีพารามิเตอร์เริ่มต้นได้หรือไม่?


164

ถ้าฉันประกาศคลาสฐาน (หรือคลาสอินเตอร์เฟส) และระบุค่าเริ่มต้นสำหรับพารามิเตอร์อย่างน้อยหนึ่งคลาสคลาสที่ได้รับจะต้องระบุค่าเริ่มต้นเดียวกันและถ้าไม่ค่าเริ่มต้นใดจะปรากฏในคลาสที่ได้รับ?

ภาคผนวก: ฉันยังสนใจที่จะจัดการสิ่งนี้ในคอมไพเลอร์ที่แตกต่างกันและการใส่ข้อมูลใด ๆ เกี่ยวกับการปฏิบัติ "แนะนำ" ในสถานการณ์นี้


1
ดูเหมือนจะเป็นเรื่องง่ายที่จะทดสอบ คุณเคยลองไหม
andand

22
ฉันอยู่ในขั้นตอนการทดลอง แต่ฉันไม่พบข้อมูลที่เป็นรูปธรรมว่าพฤติกรรม "กำหนด" จะเป็นอย่างไรในที่สุดฉันจะหาคำตอบสำหรับคอมไพเลอร์เฉพาะของฉัน แต่จะไม่บอกฉันว่าคอมไพเลอร์ทั้งหมดจะทำเช่นเดียวกัน สิ่ง. ฉันยังสนใจในการปฏิบัติที่แนะนำ
Arnold Spence

1
พฤติกรรมมีการกำหนดไว้อย่างดีและฉันสงสัยว่าคุณจะพบคอมไพเลอร์ที่ทำให้มันผิด (ถ้าคุณทดสอบ gcc 1.x หรือ VC ++ 1.0 หรืออะไรทำนองนั้น) การปฏิบัติที่แนะนำขัดต่อการทำเช่นนี้
Jerry Coffin

คำตอบ:


213

เสมือนอาจมีค่าเริ่มต้น ค่าเริ่มต้นในชั้นฐานไม่ได้รับการสืบทอดโดยชั้นเรียนที่ได้รับ

ใช้ค่าเริ่มต้นใด - เช่นคลาสพื้นฐาน 'หรือคลาสที่ได้รับ' - จะถูกกำหนดโดยประเภทสแตติกที่ใช้ในการเรียกใช้ฟังก์ชัน ถ้าคุณเรียกใช้ผ่านวัตถุคลาสพื้นฐานตัวชี้หรือการอ้างอิงจะใช้การแทนค่าเริ่มต้นในคลาสพื้นฐาน ในทางกลับกันถ้าคุณเรียกใช้คลาสวัตถุที่ได้รับตัวชี้หรือการอ้างอิงค่าเริ่มต้นที่แสดงในคลาสที่ได้รับจะถูกใช้ มีตัวอย่างด้านล่างใบเสนอราคามาตรฐานที่แสดงให้เห็นนี้

คอมไพเลอร์บางคนอาจทำสิ่งที่แตกต่างกัน แต่นี่คือสิ่งที่มาตรฐาน 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 เพียงเพื่อความกะทัดรัด - 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

ขอบคุณสำหรับการอ้างอิงซึ่งบอกฉันถึงพฤติกรรมที่ฉันคาดหวังได้ในคอมไพเลอร์ (ฉันหวังว่า)
Arnold Spence

นี่คือการแก้ไขข้อสรุปก่อนหน้าของฉัน: ฉันจะยอมรับคำตอบนี้สำหรับการอ้างอิงและกล่าวว่าคำแนะนำแบบรวมคือการมีพารามิเตอร์เริ่มต้นในฟังก์ชันเสมือนตราบใดที่พวกเขาไม่เปลี่ยนพารามิเตอร์เริ่มต้นที่ระบุไว้ในบรรพบุรุษ ชั้น
Arnold Spence

ฉันใช้ gcc 4.8.1 และฉันไม่ได้รับข้อผิดพลาดในการรวบรวม "จำนวนอาร์กิวเมนต์ที่ไม่ถูกต้อง" !!! ผมใช้เวลาวันครึ่งถึงพบข้อผิดพลาดที่ ...
Steffen

2
แต่มีเหตุผลอะไรบ้าง? ทำไมมันถูกกำหนดโดยประเภทคงที่?
user1289

2
Clang-tidy ปฏิบัติต่อพารามิเตอร์เริ่มต้นในวิธีเสมือนเป็นสิ่งที่ไม่พึงประสงค์และออกคำเตือนเกี่ยวกับสิ่งนั้น: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/ …
Martin Pecka

38

นี่คือหัวข้อของหนึ่งในปราชญ์ต้นสัปดาห์ของ 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();
}

คุณควรได้รับ A :: foo1 B :: foo2 B :: foo1


7
ขอบคุณ "อย่าทำอย่างนั้น" จาก Herb Sutter มีน้ำหนักอยู่บ้าง
Arnold Spence

2
@ArnoldSpence ในความเป็นจริง Herb Sutter เป็นมากกว่าคำแนะนำนี้ เขาเชื่อว่าอินเตอร์เฟซที่ไม่ควรมีวิธีการเสมือนที่ทั้งหมด: gotw.ca/publications/mill18.htm เมื่อวิธีการของคุณเป็นรูปธรรมและไม่สามารถ (ไม่ควร) ถูกแทนที่ก็ปลอดภัยที่จะให้พารามิเตอร์เริ่มต้น
Mark Ransom

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
มันไม่ซับซ้อนเลย พารามิเตอร์เริ่มต้นจะถูกค้นพบพร้อมกับการจำแนกชื่อ พวกเขาปฏิบัติตามกฎเดียวกัน
Edward Strange

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: [มองอย่างไร้เดียงสา] รั่วไหลอะไร? :-)
Jerry Coffin

ฉันคิดว่าเขาหมายถึงการขาดตัวทำลายล้างเสมือนจริง แต่ในกรณีนี้มันจะไม่รั่วไหล
John Dibling

1
@Jerry, destructor เป็นเสมือนถ้าคุณกำลังลบวัตถุที่ได้รับแม้ว่าตัวชี้คลาสพื้นฐาน มิฉะนั้นตัวทำลายคลาสพื้นฐานจะถูกเรียกใช้สำหรับพวกเขาทั้งหมด ในที่นี้มันก็โอเคเพราะไม่มีคนทำลายล้าง :-)
chappar

2
@ จอห์น: เดิมไม่มีการลบซึ่งเป็นสิ่งที่ฉันหมายถึง ฉันเพิกเฉยต่อการขาด destructor เสมือนจริงโดยสิ้นเชิง และ ... @chappar: ไม่ไม่เป็นไร มันต้องมี destructor เสมือนที่จะลบผ่านคลาสพื้นฐานหรือคุณได้รับพฤติกรรมที่ไม่ได้กำหนด (รหัสนี้มีลักษณะการทำงานที่ไม่ได้กำหนด) มันไม่มีส่วนเกี่ยวข้องกับข้อมูลหรือตัวทำลายที่คลาสที่ได้รับมา
GManNickG

@Chappar: รหัสเดิมไม่ได้ลบอะไรเลย แม้ว่าส่วนใหญ่จะไม่เกี่ยวข้องกับคำถามที่อยู่ในมือ แต่ฉันได้เพิ่ม dtor เสมือนลงในคลาสฐาน - ด้วย dtor เล็กน้อยมันไม่ค่อยสำคัญ แต่ GMan นั้นถูกต้องทั้งหมดหากไม่มีรหัสนี้มี UB
Jerry Coffin

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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.