สมาชิกเสมือนแบบ C ++ คงที่?


140

เป็นไปได้ใน C ++ ที่จะมีฟังก์ชั่นสมาชิกที่มีทั้งstaticและvirtual? เห็นได้ชัดว่าไม่มีวิธีที่ตรงไปตรงมาที่จะทำมัน ( static virtual member();เป็นข้อผิดพลาดในการคอมไพล์) แต่อย่างน้อยก็มีวิธีที่จะบรรลุผลเช่นเดียวกันหรือไม่?

IE:

struct Object
{
     struct TypeInformation;

     static virtual const TypeInformation &GetTypeInformation() const;
};

struct SomeObject : public Object
{
     static virtual const TypeInformation &GetTypeInformation() const;
};

มันสมเหตุสมผลที่จะใช้GetTypeInformation()ทั้งในอินสแตนซ์ ( object->GetTypeInformation()) และคลาส ( SomeObject::GetTypeInformation()) ซึ่งอาจมีประโยชน์สำหรับการเปรียบเทียบและจำเป็นสำหรับเทมเพลต

วิธีเดียวที่ฉันนึกถึงเกี่ยวข้องกับการเขียนสองฟังก์ชั่น / ฟังก์ชั่นและค่าคงที่ต่อคลาสหรือใช้มาโคร

ทางออกอื่น ๆ


12
เพียงความเห็นด้านข้าง: วิธีการคงที่ไม่ได้ดำเนินการกับอินสแตนซ์ใด ๆ ซึ่งหมายความว่าพวกเขาไม่มีตัวชี้นี้โดยนัย ที่ถูกกล่าวว่าconstในลายเซ็นวิธีการตั้งค่าสถานะthisตัวชี้นัยเป็นค่าคงที่และไม่สามารถนำไปใช้กับวิธีการคงที่เมื่อพวกเขาขาดพารามิเตอร์โดยนัย
David Rodríguez - dribeas

2
@cvb: ฉันจะพิจารณาการแทนที่ตัวอย่างของคุณอย่างจริงจังด้วยโค้ดที่ไม่เกี่ยวข้องกับการสะท้อน ตอนนี้คุณกำลังแยกแยะประเด็นสองประเด็นแยกจากกัน (แม้ว่าจะเกี่ยวข้องกับ) ใช่และฉันรู้ว่ามันเป็นเวลา 5 ปีครึ่งตั้งแต่คุณถามมัน
einpoklum

หนึ่งในคุณสมบัติที่จำเป็นโดยปริยายในที่นี้คือให้คอมไพเลอร์ตรวจสอบว่าแต่ละอ็อบเจ็กต์ในลำดับชั้นใช้อินเตอร์เฟสเฉพาะ (โดยที่หนึ่งหรือมากกว่าหนึ่งวิธีนั้นเป็นแบบสแตติก) โดยพื้นฐานแล้วการตรวจสอบเสมือนบริสุทธิ์สำหรับวิธีสแตติกทำให้รู้สึกอย่างมากเพราะถ้าคุณลืมที่จะเพิ่มวิธีการแบบคงที่แล้วคอมไพเลอร์ควรผิดพลาด virtual ไม่ใช่คำหลักที่นี่มันเป็นนามธรรมมากกว่าซึ่งเกิดขึ้นเป็นคำเหมือนใน C ++ ยกเว้นสำหรับกรณีนี้ น่าเสียดายที่คุณไม่สามารถทำได้ด้วย C ++ ในขณะนี้
xryl669

คำตอบ:


75

ไม่มีทางที่จะทำมันไม่ได้เนื่องจากสิ่งที่จะเกิดขึ้นเมื่อคุณเรียกว่าObject::GetTypeInformation()? ไม่สามารถรู้ได้ว่าจะเรียกคลาสรุ่นใดเนื่องจากไม่มีวัตถุที่เกี่ยวข้อง

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


8
หากคุณคิดว่าระดับคงที่ (หรือเรียนสมาชิกคงที่) เป็นเดี่ยวทุกอย่างจะกลายเป็นที่เห็นได้ชัด - ในกรณีของคุณเพียงวัตถุ :: GetTypeInformation ควรจะเรียกว่า - ลักษณะเดียวกับโทรวิธีเสมือนปกติในชั้นฐานเช่น (แน่นอนถ้า C ++ สนับสนุนวิธีการคงที่เสมือนจริง)
Spook

13
นั่นเป็นข้อโต้แย้งที่กว้างขวางอย่างสมบูรณ์ ถ้าคุณใช้คลาสแทนวัตถุมันจะใช้เวอร์ชันจากคลาสนั้นโดยธรรมชาติแทนที่จะทำการส่งแบบเสมือน ไม่มีอะไรใหม่ที่นั่น
Deduplicator

54

หลายคนบอกว่ามันเป็นไปไม่ได้ฉันจะไปอีกขั้นหนึ่งแล้วพูดว่ามันไม่มีความหมายเต็ม

สมาชิกแบบคงที่คือสิ่งที่ไม่เกี่ยวข้องกับอินสแตนซ์ใด ๆ เฉพาะกับคลาส

สมาชิกเสมือนเป็นสิ่งที่ไม่เกี่ยวข้องโดยตรงกับคลาสใด ๆ กับอินสแตนซ์เท่านั้น

ดังนั้นสมาชิกเสมือนคงที่จะเป็นสิ่งที่ไม่เกี่ยวข้องกับอินสแตนซ์หรือคลาสใด ๆ


42
มันมีความหมายอย่างสมบูรณ์ในภาษาที่ชั้นเรียนมีค่าชั้นหนึ่ง - เช่น Delphi มีสิ่งนั้นและยังมีวิธี "แบบคงที่เสมือน"
Pavel Minaev

4
เผง "ฟังก์ชั่นเสมือนจริง" คือ (ตามคำจำกัดความ) ฟังก์ชั่นที่เชื่อมโยงแบบไดนามิกนั่นคือมันถูกเลือกที่รันไทม์ขึ้นอยู่กับประเภทไดนามิกของวัตถุที่กำหนด ดังนั้นไม่มี object = no การโทรเสมือน
Kos

7
ฉันคิดว่า virtual virtual static มีความหมายเช่นกัน มันจะเป็นไปได้ที่จะกำหนดคลาสอินเตอร์เฟสและรวมถึงวิธีการคงที่ที่จะต้องดำเนินการในชั้นเรียนที่ได้รับ
bkausbk

34
มันไม่ได้มีความหมายสำหรับstatic virtualวิธีการ แต่วิธีการที่static บริสุทธิ์ virtualมีความหมายมากในส่วนติดต่อ
Bret Kuhns

4
static const string MyClassSillyAdditionalNameมันเป็นอย่างดีมีความหมายที่จะมี
einpoklum

23

ฉันพบปัญหานี้เมื่อวันก่อน: ฉันมีบางคลาสที่เต็มไปด้วยวิธีการคงที่ แต่ฉันต้องการใช้การสืบทอดและวิธีการเสมือนจริงและลดการทำซ้ำรหัส ทางออกของฉันคือ:

แทนที่จะใช้วิธีคงที่ให้ใช้ซิงเกิลตันด้วยวิธีเสมือน

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

ในทางปฏิบัติการใช้ซิงเกิลตันนั้นเหมือนกับการใช้วิธีการสแตติกยกเว้นว่าคุณสามารถใช้ประโยชน์จากการสืบทอดและวิธีเสมือน


นั่นจะทำให้ฉันเสียค่าใช้จ่าย - เว้นแต่ว่าคอมไพเลอร์สามารถมั่นใจได้ว่า: 1. จริง ๆ แล้วมันเป็นซิงเกิลตันและ 2 ไม่มีอะไรที่สืบทอดมาจากมันฉันไม่คิดว่ามันจะเพิ่มประสิทธิภาพของค่าใช้จ่ายทั้งหมด
einpoklum

หากประสิทธิภาพของสิ่งนี้เป็นกังวลคุณ C # น่าจะเป็นภาษาที่ไม่ถูกต้องสำหรับคุณ
Nate CK

3
อ่าจุดดี เห็นได้ชัดว่าไม่นานมานี้ที่ฉันคิดเกี่ยวกับเรื่องนี้ตั้งแต่ฉันเขียนมันในปี 2009 ขอผมพูดอีกอย่างหนึ่ง: หากสิ่งที่การแสดงแบบนี้ทำให้คุณกังวลคุณอาจจะหลีกเลี่ยงการใช้มรดกโดยสิ้นเชิง ผู้โพสต์ถามหาวิธีเสมือนโดยเฉพาะดังนั้นจึงแปลกที่คุณมาที่นี่เพื่อบ่นเกี่ยวกับค่าใช้จ่ายของวิธีเสมือน
Nate CK

15

มันเป็นไปได้!

แต่สิ่งที่เป็นไปได้จริงลองแคบลง ผู้คนมักต้องการ "ฟังก์ชั่นเสมือนจริงแบบคงที่" เพราะการทำซ้ำรหัสที่จำเป็นสำหรับการเรียกฟังก์ชั่นเดียวกันผ่านสายคงที่ "SomeDerivedClass :: myfunction ()" และการเรียก polymorphic "base_class_pointer-> myfunction ()" วิธีการ "ทางกฎหมาย" สำหรับการอนุญาตการทำงานดังกล่าวเป็นการทำซ้ำคำจำกัดความของฟังก์ชัน:

class Object
{
public:
    static string getTypeInformationStatic() { return "base class";}
    virtual string getTypeInformation() { return getTypeInformationStatic(); }
}; 
class Foo: public Object
{
public:
    static string getTypeInformationStatic() { return "derived class";}
    virtual string getTypeInformation() { return getTypeInformationStatic(); }
};

จะทำอย่างไรถ้าคลาสพื้นฐานมีฟังก์ชันสแตติกจำนวนมากและคลาสที่ได้รับจะต้องแทนที่พวกเขาทุกคนและหนึ่งลืมที่จะให้คำจำกัดความที่ซ้ำกันสำหรับฟังก์ชั่นเสมือน ใช่เราจะได้รับข้อผิดพลาดแปลก ๆ ในระหว่างรันไทม์ซึ่งยากต่อการติดตาม ทำให้การทำซ้ำรหัสเป็นสิ่งที่ไม่ดี ต่อไปนี้พยายามที่จะแก้ไขปัญหานี้ (และฉันต้องการบอกล่วงหน้าว่ามันปลอดภัยอย่างสมบูรณ์ประเภทและไม่มีเวทมนตร์สีดำใด ๆ เช่น typeid หรือ dynamic_cast ของ :)

ดังนั้นเราต้องการให้คำจำกัดความเพียงหนึ่งของ getTypeInformation () ต่อคลาสที่ได้รับและเป็นที่ชัดเจนว่าจะต้องมีคำจำกัดความของสแตติกฟังก์ชั่นเพราะมันเป็นไปไม่ได้ที่จะเรียกว่า "SomeDerivedClass :: getTypeInformation ()" ถ้า getTypeInformation () เป็นเสมือน เราจะเรียกฟังก์ชันสแตติกของคลาสที่ได้รับผ่านตัวชี้ไปยังคลาสพื้นฐานได้อย่างไร? มันเป็นไปไม่ได้กับ vtable เพราะ vtable เก็บตัวชี้ไปที่ฟังก์ชั่นเสมือนเท่านั้นและเนื่องจากเราตัดสินใจที่จะไม่ใช้ฟังก์ชั่นเสมือนจริงเราจึงไม่สามารถปรับเปลี่ยน vtable เพื่อประโยชน์ของเราได้ จากนั้นเพื่อให้สามารถเข้าถึงฟังก์ชั่นแบบคงที่สำหรับคลาสที่ได้รับผ่านตัวชี้ไปยังคลาสพื้นฐานเราต้องจัดเก็บชนิดของวัตถุภายในคลาสฐานของมัน วิธีหนึ่งคือการทำให้คลาสฐานเทมเพลทโดยใช้ "รูปแบบเทมเพลตที่เกิดซ้ำอย่างน่าสนใจ" แต่มันไม่เหมาะสมที่นี่และเราจะใช้เทคนิคที่เรียกว่า "การลบประเภท":

class TypeKeeper
{
public:
    virtual string getTypeInformation() = 0;
};
template<class T>
class TypeKeeperImpl: public TypeKeeper
{
public:
    virtual string getTypeInformation() { return T::getTypeInformationStatic(); }
};

ตอนนี้เราสามารถจัดเก็บประเภทของวัตถุภายในคลาส "Object" ฐานกับตัวแปร "ผู้รักษา":

class Object
{
public:
    Object(){}
    boost::scoped_ptr<TypeKeeper> keeper;

    //not virtual
    string getTypeInformation() const 
    { return keeper? keeper->getTypeInformation(): string("base class"); }

};

ในผู้รักษาระดับที่ได้รับจะต้องเริ่มต้นในระหว่างการก่อสร้าง:

class Foo: public Object
{
public:
    Foo() { keeper.reset(new TypeKeeperImpl<Foo>()); }
    //note the name of the function
    static string getTypeInformationStatic() 
    { return "class for proving static virtual functions concept"; }
};

มาเพิ่มน้ำตาลซินแทคติค:

template<class T>
void override_static_functions(T* t)
{ t->keeper.reset(new TypeKeeperImpl<T>()); }
#define OVERRIDE_STATIC_FUNCTIONS override_static_functions(this)

ตอนนี้การประกาศของลูกหลานดูเหมือนว่า:

class Foo: public Object
{
public:
    Foo() { OVERRIDE_STATIC_FUNCTIONS; }
    static string getTypeInformationStatic() 
    { return "class for proving static virtual functions concept"; }
};

class Bar: public Foo
{
public:
    Bar() { OVERRIDE_STATIC_FUNCTIONS; }
    static string getTypeInformationStatic() 
    { return "another class for the same reason"; }
};

การใช้งาน:

Object* obj = new Foo();
cout << obj->getTypeInformation() << endl;  //calls Foo::getTypeInformationStatic()
obj = new Bar();
cout << obj->getTypeInformation() << endl;  //calls Bar::getTypeInformationStatic()
Foo* foo = new Bar();
cout << foo->getTypeInformation() << endl; //calls Bar::getTypeInformationStatic()
Foo::getTypeInformation(); //compile-time error
Foo::getTypeInformationStatic(); //calls Foo::getTypeInformationStatic()
Bar::getTypeInformationStatic(); //calls Bar::getTypeInformationStatic()

ข้อดี:

  1. การทำซ้ำรหัสน้อย (แต่เราต้องเรียก OVERRIDE_STATIC_FUNCTIONS ในตัวสร้างทุกตัว)

ข้อเสีย:

  1. OVERRIDE_STATIC_FUNCTIONS ในตัวสร้างทุกตัว
  2. หน่วยความจำและค่าใช้จ่ายประสิทธิภาพ
  3. เพิ่มความซับซ้อน

เปิดประเด็น:

1) มีชื่อแตกต่างกันสำหรับฟังก์ชั่นคงที่และเสมือนวิธีการแก้ปัญหาความกำกวมที่นี่?

class Foo
{
public:
    static void f(bool f=true) { cout << "static";}
    virtual void f() { cout << "virtual";}
};
//somewhere
Foo::f(); //calls static f(), no ambiguity
ptr_to_foo->f(); //ambiguity

2) วิธีการโทร OVERRIDE_STATIC_FUNCTIONS โดยนัยภายในตัวสร้างทุกตัว


+1 สำหรับความพยายามถึงแม้ว่าฉันไม่แน่ใจว่านี่จะงดงามกว่าเพียงแค่มอบหมายฟังก์ชันการทำงานให้กับซิงเกิลตันด้วยวิธีการเสมือน
einpoklum

1
@ einpoklum ฉันสามารถคิดถึงสถานการณ์เมื่อสิ่งนี้ดีกว่า สมมติว่าเรามีรหัสลูกค้าจำนวนมากที่เรียกวิธีการคงที่แล้ว การเปลี่ยนจากสแตติกเมธอดเป็นซิงเกิลตันด้วยเมธอดเสมือนจะต้องมีการเปลี่ยนแปลงในรหัสลูกค้าในขณะที่วิธีการที่นำเสนอข้างต้นนั้นไม่รุกราน
Alsk

ไม่จำเป็นต้องใช้คีย์เวิร์ด "virtual" สำหรับ "Foo :: getTypeInformation" และ "TypeKeeperImpl :: getTypeInformation"
bartolo-otrit

12

ในขณะที่ Alsk ได้ให้คำตอบโดยละเอียดแล้วฉันต้องการเพิ่มทางเลือกอื่นเนื่องจากฉันคิดว่าการใช้งานที่ได้รับการปรับปรุงของเขานั้นซับซ้อนเกินไป

เราเริ่มต้นด้วยคลาสฐานนามธรรมซึ่งจัดเตรียมอินเตอร์เฟสสำหรับชนิดอ็อบเจ็กต์ทั้งหมด:

class Object
{
public:
    virtual char* GetClassName() = 0;
};

ตอนนี้เราต้องการการใช้งานจริง แต่เพื่อหลีกเลี่ยงการเขียนทั้งวิธีคงที่และวิธีเสมือนเราจะมีคลาสวัตถุจริงของเราสืบทอดวิธีเสมือน เห็นได้ชัดว่ามันใช้งานได้ถ้าคลาสฐานรู้วิธีเข้าถึงฟังก์ชันสมาชิกแบบคงที่ ดังนั้นเราต้องใช้เทมเพลตและส่งชื่อคลาสของวัตถุจริงให้กับมัน:

template<class ObjectType>
class ObjectImpl : public Object
{
public:
    virtual char* GetClassName()
    {
        return ObjectType::GetClassNameStatic();
    }
};

ในที่สุดเราจำเป็นต้องใช้วัตถุจริงของเรา ที่นี่เราเพียงแค่ต้องใช้ฟังก์ชั่นสมาชิกคงที่ฟังก์ชั่นสมาชิกเสมือนจะได้รับมรดกจากคลาสเทมเพลต ObjectImpl สร้างอินสแตนซ์ที่มีชื่อของคลาสที่ได้รับดังนั้นมันจะเข้าถึงสมาชิกแบบคงที่ของมัน

class MyObject : public ObjectImpl<MyObject>
{
public:
    static char* GetClassNameStatic()
    {
        return "MyObject";
    }
};

class YourObject : public ObjectImpl<YourObject>
{
public:
    static char* GetClassNameStatic()
    {
        return "YourObject";
    }
};

ลองเพิ่มรหัสเพื่อทดสอบ:

char* GetObjectClassName(Object* object)
{
    return object->GetClassName();
}

int main()
{
    MyObject myObject;
    YourObject yourObject;

    printf("%s\n", MyObject::GetClassNameStatic());
    printf("%s\n", myObject.GetClassName());
    printf("%s\n", GetObjectClassName(&myObject));
    printf("%s\n", YourObject::GetClassNameStatic());
    printf("%s\n", yourObject.GetClassName());
    printf("%s\n", GetObjectClassName(&yourObject));

    return 0;
}

ภาคผนวก (12 มกราคม 2019):

แทนที่จะใช้ฟังก์ชัน GetClassNameStatic () คุณยังสามารถกำหนดชื่อคลาสเป็นสมาชิกแบบสแตติกแม้ "inline" ซึ่ง IIRC ทำงานได้ตั้งแต่ C ++ 11 (ไม่ต้องกลัวตัวปรับแต่งทั้งหมด :)):

class MyObject : public ObjectImpl<MyObject>
{
public:
    // Access this from the template class as `ObjectType::s_ClassName` 
    static inline const char* const s_ClassName = "MyObject";

    // ...
};

11

มันเป็นไปได้. ทำให้สองฟังก์ชั่น: คงที่และเสมือน

struct Object{     
  struct TypeInformation;
  static  const TypeInformation &GetTypeInformationStatic() const 
  { 
      return GetTypeInformationMain1();
  }
  virtual const TypeInformation &GetTypeInformation() const
  { 
      return GetTypeInformationMain1();
  }
protected:
  static const TypeInformation &GetTypeInformationMain1(); // Main function
};

struct SomeObject : public Object {     
  static  const TypeInformation &GetTypeInformationStatic() const 
  { 
      return GetTypeInformationMain2();
  }
  virtual const TypeInformation &GetTypeInformation() const
  { 
      return GetTypeInformationMain2();
  }
protected:
  static const TypeInformation &GetTypeInformationMain2(); // Main function
};

4
นอกจากนี้วิธีการคงที่ไม่สามารถ const มันไม่เข้าท่าพวกเขาจะไม่กลายพันธุ์อินสแตนซ์อะไร
David Rodríguez - dribeas

1
นี่เป็นเพียงการคัดลอกโค้ดส่วนใหญ่ แนวคิดนี้มีไว้สำหรับคลาสย่อยที่จะต้องมีสมาชิก const แบบคงที่เท่านั้นไม่จำเป็นต้องมีรหัสในการเข้าถึง
einpoklum

8

ไม่เป็นไปไม่ได้เพราะฟังก์ชั่นสมาชิกแบบคงที่ไม่มีthisตัวชี้ และสมาชิกแบบสแตติก (ทั้งฟังก์ชั่นและตัวแปร) ไม่ใช่สมาชิกชั้นเรียนจริงๆ พวกเขาเพิ่งถูกเรียกใช้ClassName::memberและยึดตามตัวระบุการเข้าถึงคลาส ที่เก็บข้อมูลของพวกเขาถูกกำหนดไว้ที่ไหนสักแห่งนอกห้องเรียน ไม่ได้สร้างที่เก็บข้อมูลในแต่ละครั้งที่คุณสร้างวัตถุของคลาส ตัวชี้ไปยังสมาชิกชั้นเรียนเป็นพิเศษในความหมายและไวยากรณ์ ตัวชี้ไปยังสมาชิกแบบคงที่เป็นตัวชี้ปกติในทุกเรื่อง

ฟังก์ชั่นเสมือนจริงในคลาสต้องการthisตัวชี้และอยู่คู่กับคลาสจึงไม่สามารถคงที่ได้


1
เฉพาะฟังก์ชันที่ไม่คงที่เท่านั้นที่ต้องการthis ตัวชี้ ฟังก์ชั่นสแตติกไม่ได้เฉพาะเจาะจงกับอินสแตนซ์และไม่ต้องการมัน ดังนั้น - นั่นไม่ใช่เหตุผลสมาชิกเสมือนคงเป็นไปไม่ได้
einpoklum

7

ค่อนข้างคำตอบที่ล่าช้า แต่เป็นไปได้โดยใช้รูปแบบแม่แบบที่เกิดซ้ำอยากรู้อยากเห็น นี้วิกิพีเดียบทความมีข้อมูลที่คุณต้องการและยังคงอยู่ภายใต้การตัวอย่างเช่นความแตกต่างเป็นสิ่งที่คุณกำลังถามหา


3

ฉันคิดว่าสิ่งที่คุณพยายามทำสามารถทำได้ผ่านเทมเพลต ฉันพยายามอ่านระหว่างบรรทัดที่นี่ สิ่งที่คุณพยายามจะทำคือการเรียกวิธีการจากรหัสบางอย่างที่มันเรียกรุ่นที่ได้รับมา แต่ผู้โทรไม่ได้ระบุชั้นเรียน ตัวอย่าง:

class Foo {
public:
    void M() {...}
};

class Bar : public Foo {
public:
    void M() {...}
};

void Try()
{
    xxx::M();
}

int main()
{
    Try();
}

คุณต้องการลอง () เพื่อโทรหา M เวอร์ชันบาร์โดยไม่ระบุบาร์ วิธีที่คุณทำเพื่อสถิตยศาสตร์คือการใช้เทมเพลต ดังนั้นเปลี่ยนให้เป็นเช่น:

class Foo {
public:
    void M() {...}
};

class Bar : public Foo {
public:
    void M() {...}
};

template <class T>
void Try()
{
    T::M();
}

int main()
{
    Try<Bar>();
}

1
หากคุณเว้นวรรคโค้ดของคุณ 4 คุณจะได้รับการฟอร์แมตโดยอัตโนมัติ อีกทางเลือกหนึ่งฉันเชื่อว่าคุณสามารถใช้เส้นขีดหลังเพื่อบรรลุวัตถุประสงค์เดียวกันได้
chollida

1
นี่คือสิ่งที่ชัดเจนที่ฉันคิดถึง ขอบคุณ. ยังคงสมาชิกpubicแปลก
allesblinkt

M () ไม่ใช่ฟังก์ชันคงที่ มันเรียกว่า T :: M () อย่างไร?
DDukDDak99

3

ไม่ฟังก์ชันสมาชิกแบบคงที่ไม่สามารถเสมือนได้แนวคิดเสมือนจริงได้รับการแก้ไขในขณะใช้งานด้วยความช่วยเหลือของ vptr และ vptr ไม่ได้เป็นสมาชิกแบบคงที่ของ class.due กับฟังก์ชั่นสมาชิกแบบคงที่ไม่สามารถ acess vptr สมาชิกแบบคงที่ ไม่ใช่เสมือน


2
เมธอดเสมือนเฉพาะอินสแตนซ์เท่านั้นที่ต้องการ vtable ของอินสแตนซ์ คุณสามารถมีสแตติก - หนึ่งต่อคลาส - vtable และถ้าคุณต้องการให้อินสแตนซ์รู้เพียงแค่ชี้จาก vtable ของอินสแตนซ์ไปยังคลาสสแตติกของ vtable
einpoklum

2

มันเป็นไปไม่ได้ แต่นั่นเป็นเพราะการละเลย ไม่ใช่สิ่งที่ "ไม่สมเหตุสมผล" เนื่องจากผู้คนจำนวนมากดูเหมือนจะอ้างสิทธิ์ เพื่อความชัดเจนฉันกำลังพูดถึงสิ่งนี้:

struct Base {
  static virtual void sayMyName() {
    cout << "Base\n";
  }
};

struct Derived : public Base {
  static void sayMyName() override {
    cout << "Derived\n";
  }
};

void foo(Base *b) {
  b->sayMyName();
  Derived::sayMyName(); // Also would work.
}

นี่คือบางสิ่งที่สามารถนำไปใช้งานได้100% (แต่ไม่ได้ใช้) และฉันจะเถียงกับบางสิ่งที่มีประโยชน์

พิจารณาว่าฟังก์ชั่นเสมือนปกติทำงานอย่างไร ลบstatics และเพิ่มสิ่งอื่น ๆ และเรามี:

struct Base {
  virtual void sayMyName() {
    cout << "Base\n";
  }
  virtual void foo() {
  }
  int somedata;
};

struct Derived : public Base {
  void sayMyName() override {
    cout << "Derived\n";
  }
};

void foo(Base *b) {
  b->sayMyName();
}

สิ่งนี้ทำงานได้ดีและสิ่งที่เกิดขึ้นคือคอมไพเลอร์สร้างสองตารางเรียกว่า VTables และกำหนดดัชนีให้กับฟังก์ชันเสมือนเช่นนี้

enum Base_Virtual_Functions {
  sayMyName = 0;
  foo = 1;
};

using VTable = void*[];

const VTable Base_VTable = {
  &Base::sayMyName,
  &Base::foo
};

const VTable Derived_VTable = {
  &Derived::sayMyName,
  &Base::foo
};

ถัดไปแต่ละคลาสที่มีฟังก์ชั่นเสมือนจริงจะถูกเพิ่มด้วยฟิลด์อื่นที่ชี้ไปที่ VTable ดังนั้นคอมไพเลอร์จะเปลี่ยนพวกเขาเป็นดังนี้

struct Base {
  VTable* vtable;
  virtual void sayMyName() {
    cout << "Base\n";
  }
  virtual void foo() {
  }
  int somedata;
};

struct Derived : public Base {
  VTable* vtable;
  void sayMyName() override {
    cout << "Derived\n";
  }
};

แล้วสิ่งที่เกิดขึ้นจริงเมื่อคุณโทรb->sayMyName()? โดยทั่วไปสิ่งนี้:

b->vtable[Base_Virtual_Functions::sayMyName](b);

(พารามิเตอร์แรกกลายเป็นthis)

โอเคแล้วมันจะทำงานกับฟังก์ชั่นเสมือนจริงได้อย่างไร? ความแตกต่างระหว่างฟังก์ชั่นสมาชิกแบบคงที่และไม่คงที่คืออะไร? ข้อแตกต่างคือหลังได้รับthisตัวชี้

เราสามารถทำสิ่งเดียวกันกับฟังก์ชันเสมือนแบบสแตติก - เพียงแค่ลบthisตัวชี้

b->vtable[Base_Virtual_Functions::sayMyName]();

สิ่งนี้สามารถรองรับไวยากรณ์ทั้งสอง:

b->sayMyName(); // Prints "Base" or "Derived"...
Base::sayMyName(); // Always prints "Base".

ดังนั้นอย่าเพิกเฉยต่อบรรดาผู้มีอิทธิพล มันไม่เข้าท่า ทำไมจึงไม่รองรับ ฉันคิดว่าเป็นเพราะมีประโยชน์น้อยมากและอาจทำให้สับสนเล็กน้อย

ข้อได้เปรียบทางเทคนิคเพียงอย่างเดียวของฟังก์ชั่นเสมือนปกติคือคุณไม่จำเป็นต้องผ่านthisไปยังฟังก์ชั่น แต่ฉันไม่คิดว่ามันจะสร้างความแตกต่างที่วัดได้ให้กับประสิทธิภาพ

หมายความว่าคุณไม่มีฟังก์ชั่นคงที่และไม่คงที่แยกกันสำหรับกรณีที่คุณมีอินสแตนซ์และเมื่อคุณไม่มีอินสแตนซ์ แต่ก็อาจสับสนว่าเป็นเพียง "เสมือน" จริง ๆ เมื่อคุณใช้ การเรียกอินสแตนซ์


0

ไม่เป็นไปไม่ได้เนื่องจากสมาชิกแบบสแตติกจะถูกผูกไว้ที่เวลาคอมไพล์ขณะที่สมาชิกเสมือนถูกผูกไว้ที่รันไทม์


0

ข้อแรกคำตอบที่ถูกต้องคือสิ่งที่ OP กำลังร้องขอคือความขัดแย้งในแง่: วิธีเสมือนขึ้นอยู่กับชนิดของเวลาทำงานของอินสแตนซ์ ฟังก์ชั่นแบบคงที่ไม่ได้ขึ้นอยู่กับอินสแตนซ์โดยเฉพาะ ที่กล่าวว่ามันทำให้รู้สึกถึงฟังก์ชั่นคงที่คืนสิ่งที่เฉพาะกับประเภท ตัวอย่างเช่นฉันมีตระกูลคลาส MouseTool สำหรับรูปแบบสถานะและฉันเริ่มให้แต่ละรายการมีฟังก์ชันแบบคงที่ที่ส่งคืนตัวดัดแปลงคีย์บอร์ดที่ไปพร้อมกับมัน ฉันใช้ฟังก์ชั่นแบบคงที่เหล่านั้นในฟังก์ชั่นจากโรงงานที่ทำอินสแตนซ์ MouseTool ที่ถูกต้อง ฟังก์ชั่นนั้นตรวจสอบสถานะเมาส์กับ MouseToolA :: keyboardModifier (), MouseToolB :: keyboardModifier () และอื่น ๆ จากนั้นสร้างอินสแตนซ์ที่เหมาะสม แน่นอนในภายหลังฉันต้องการตรวจสอบว่ารัฐถูกต้องหรือไม่ดังนั้นฉันต้องการเขียนอะไรบางอย่างเช่น "

ดังนั้นหากคุณพบว่าตัวเองต้องการสิ่งนี้คุณอาจต้องการนำวิธีแก้ปัญหากลับมาใช้ใหม่ แต่ถึงกระนั้นฉันเข้าใจความปรารถนาที่จะมีวิธีการคงที่แล้วเรียกพวกเขาแบบไดนามิกขึ้นอยู่กับประเภทแบบไดนามิกของอินสแตนซ์ ฉันคิดว่ารูปแบบผู้เข้าชมสามารถให้สิ่งที่คุณต้องการ มันให้สิ่งที่คุณต้องการ มันเป็นรหัสพิเศษเล็กน้อย แต่มันอาจมีประโยชน์สำหรับผู้เยี่ยมชมคนอื่น ๆ

ดู: http://en.wikipedia.org/wiki/Visitor_patternสำหรับพื้นหลัง

struct ObjectVisitor;

struct Object
{
     struct TypeInformation;

     static TypeInformation GetTypeInformation();
     virtual void accept(ObjectVisitor& v);
};

struct SomeObject : public Object
{
     static TypeInformation GetTypeInformation();
     virtual void accept(ObjectVisitor& v) const;
};

struct AnotherObject : public Object
{
     static TypeInformation GetTypeInformation();
     virtual void accept(ObjectVisitor& v) const;
};

จากนั้นสำหรับแต่ละวัตถุที่เป็นรูปธรรม:

void SomeObject::accept(ObjectVisitor& v) const {
    v.visit(*this); // The compiler statically picks the visit method based on *this being a const SomeObject&.
}
void AnotherObject::accept(ObjectVisitor& v) const {
    v.visit(*this); // Here *this is a const AnotherObject& at compile time.
}

จากนั้นกำหนดผู้เยี่ยมชมฐาน:

struct ObjectVisitor {
    virtual ~ObjectVisitor() {}
    virtual void visit(const SomeObject& o) {} // Or = 0, depending what you feel like.
    virtual void visit(const AnotherObject& o) {} // Or = 0, depending what you feel like.
    // More virtual void visit() methods for each Object class.
};

จากนั้นผู้เข้าชมที่เป็นรูปธรรมที่เลือกฟังก์ชั่นคงที่เหมาะสม:

struct ObjectVisitorGetTypeInfo {
    Object::TypeInformation result;
    virtual void visit(const SomeObject& o) {
        result = SomeObject::GetTypeInformation();
    }
    virtual void visit(const AnotherObject& o) {
        result = AnotherObject::GetTypeInformation();
    }
    // Again, an implementation for each concrete Object.
};

ในที่สุดใช้มัน:

void printInfo(Object& o) {
    ObjectVisitorGetTypeInfo getTypeInfo;
    Object::TypeInformation info = o.accept(getTypeInfo).result;
    std::cout << info << std::endl;
}

หมายเหตุ:

  • ความกังวลทางด้านซ้ายเป็นการออกกำลังกาย
  • คุณส่งคืนการอ้างอิงจากสแตติก นอกจากว่าคุณจะมีซิงเกิลตันมันก็น่าสงสัย

หากคุณต้องการหลีกเลี่ยงข้อผิดพลาดในการคัดลอกวางซึ่งหนึ่งในวิธีการเยี่ยมชมของคุณเรียกฟังก์ชันคงที่ผิดคุณสามารถใช้ฟังก์ชันตัวช่วย templated (ซึ่งไม่สามารถเป็นเสมือนจริงได้) ผู้เข้าชมของคุณด้วยเทมเพลตเช่นนี้:

struct ObjectVisitorGetTypeInfo {
    Object::TypeInformation result;
    virtual void visit(const SomeObject& o) { doVisit(o); }
    virtual void visit(const AnotherObject& o) { doVisit(o); }
    // Again, an implementation for each concrete Object.

  private:
    template <typename T>
    void doVisit(const T& o) {
        result = T::GetTypeInformation();
    }
};

วิธีการคงที่เสมือนจริงถ้าพวกเขามีอยู่จะไม่ขึ้นอยู่กับสิ่งใดในอินสแตนซ์ - แต่อินสแตนซ์จะต้องรู้ประเภทของมันที่จะเรียกพวกเขา สิ่งนี้สามารถทำงานได้โดยคอมไพเลอร์ (เช่นโดยการใช้โครงสร้างข้อมูลเดี่ยวต่อคลาสพร้อมกับพอยน์เตอร์ไปยังเมธอดสแตติกเสมือนและสมาชิก) มันไม่ได้ขัดแย้งอย่างแน่นอน
einpoklum

ไม่ว่าจะเป็นความขัดแย้งในแง่หรือไม่เป็นคำถามของความหมาย ใคร ๆ ก็นึกออกว่า C ++ อนุญาตให้มีการเรียกสถิตจากอินสแตนซ์ได้ (เช่นFoo foo; ... foo::bar();แทนFoo::bar();) ไม่เหมือนdecltype(foo)::bar();แต่จะถูกผูกมัดแบบคงที่อีกครั้ง ดูเหมือนว่าวิธีการของผู้เยี่ยมชมเป็นวิธีที่สมเหตุสมผลในการทำให้เกิดพฤติกรรมนี้โดยไม่ทำให้วิธีคงที่นั้นเป็นวิธี const แบบเสมือน
Ben

0

ด้วย c ++ คุณสามารถใช้การสืบทอดแบบคงที่ด้วยวิธี crt ตัวอย่างเช่นมันถูกใช้อย่างกว้างขวางในเทมเพลตหน้าต่าง atl & wtl

ดูhttps://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

เพื่อความเรียบง่ายคุณมีคลาสที่เทมเพลตจากตัวเองเช่นคลาส myclass: ผู้เป็นที่รักของสาธารณะ จากจุดนี้คลาส myancestor สามารถเรียกใช้ฟังก์ชัน T :: YourImpl ของคุณได้แล้ว


-1

บางทีคุณสามารถลองโซลูชันของฉันด้านล่าง:

class Base {
public:
    Base(void);
    virtual ~Base(void);

public:
    virtual void MyVirtualFun(void) = 0;
    static void  MyStaticFun(void) { assert( mSelf != NULL); mSelf->MyVirtualFun(); }
private:
    static Base* mSelf;
};

Base::mSelf = NULL;

Base::Base(void) {
    mSelf = this;
}

Base::~Base(void) {
    // please never delete mSelf or reset the Value of mSelf in any deconstructors
}

class DerivedClass : public Base {
public:
    DerivedClass(void) : Base() {}
    ~DerivedClass(void){}

public:
    virtual void MyVirtualFun(void) { cout<<"Hello, it is DerivedClass!"<<endl; }
};

int main() {
    DerivedClass testCls;
    testCls.MyStaticFun(); //correct way to invoke this kind of static fun
    DerivedClass::MyStaticFun(); //wrong way
    return 0;
}

ใช่ฉันรู้ 4 ปี อธิบายคะแนน - สำหรับผู้ที่ไม่ต้องการอ่านโค้ดในรายละเอียดมากขนาดนั้น Base::mSelfหมายถึงการสร้างอินสแตนซ์เมื่อเร็ว ๆ นี้ส่วนใหญ่ของชั้นเรียนมาใด ๆแม้ว่าอินสแตนซ์ที่ได้รับการ destructed ดังนั้นclass D1 : public Base ...; class D2 : public Base ...; ...; D1* pd1 = new D1(); D2* pd2 = new D2(); pd1->MyStaticFun(); /* calls D2::MyVirtualFun() */ delete pd2; pd1->MyStaticFun(); /* calls via deleted pd2 */ซึ่งไม่ใช่สิ่งที่ต้องการ
Jesse Chisholm

-3

อย่างที่คนอื่นพูดมีข้อมูลสำคัญ 2 อย่าง:

  1. ไม่มีthisตัวชี้เมื่อทำการเรียกฟังก์ชันแบบสแตติกและ
  2. thisจุดที่ชี้ไปยังโครงสร้างที่ตารางเสมือนหรือ thunk จะใช้ในการมองขึ้นซึ่งรันไทม์วิธีการในการโทร

ฟังก์ชั่นคงที่จะถูกกำหนดในเวลารวบรวม

ฉันแสดงตัวอย่างรหัสนี้ในสมาชิก C + + คงที่ในชั้นเรียน ; มันแสดงให้เห็นว่าคุณสามารถเรียกวิธีการคงที่กำหนดตัวชี้โมฆะ:

struct Foo
{
    static int boo() { return 2; }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo* pFoo = NULL;
    int b = pFoo->boo(); // b will now have the value 2
    return 0;
}

6
ในทางเทคนิคนี่คือพฤติกรรมที่ไม่ได้กำหนด คุณไม่สามารถถือว่าตัวชี้เป็นโมฆะไม่ว่าด้วยเหตุผลใด สิ่งเดียวที่คุณสามารถทำได้ด้วยตัวชี้โมฆะคือ a) กำหนดตัวชี้อื่นให้กับมันและ b) เปรียบเทียบกับตัวชี้อื่น
KeithB

1
นอกจากนี้คุณสามารถเปรียบเทียบเพื่อความเท่าเทียมกัน . (หรือ inequality_ มีตัวชี้อีกไม่ได้สั่งซื้อ Ie p < null, p >= nullฯลฯ จะไม่ได้กำหนดทั้งหมดเช่นกัน.
พาเวล Minaev

1
@KeithB - ​​เพื่อความสมบูรณ์คุณยังสามารถโทรลบอย่างปลอดภัยบนตัวชี้ null
Steve Rowe
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.