Apakah fungsi virtual sebaris benar-benar tidak masuk akal?


172

Saya mendapat pertanyaan ini ketika saya menerima komentar ulasan kode yang mengatakan fungsi virtual tidak perlu sebaris.

Saya pikir fungsi virtual sebaris bisa berguna dalam skenario di mana fungsi dipanggil pada objek secara langsung. Namun argumen yang muncul di benak saya adalah - mengapa seseorang ingin mendefinisikan virtual dan kemudian menggunakan objek untuk memanggil metode?

Apakah lebih baik tidak menggunakan fungsi virtual sebaris, karena mereka hampir tidak pernah diperluas?

Cuplikan kode yang saya gunakan untuk analisis:

class Temp
{
public:

    virtual ~Temp()
    {
    }
    virtual void myVirtualFunction() const
    {
        cout<<"Temp::myVirtualFunction"<<endl;
    }

};

class TempDerived : public Temp
{
public:

    void myVirtualFunction() const
    {
        cout<<"TempDerived::myVirtualFunction"<<endl;
    }

};

int main(void) 
{
    TempDerived aDerivedObj;
    //Compiler thinks it's safe to expand the virtual functions
    aDerivedObj.myVirtualFunction();

    //type of object Temp points to is always known;
    //does compiler still expand virtual functions?
    //I doubt compiler would be this much intelligent!
    Temp* pTemp = &aDerivedObj;
    pTemp->myVirtualFunction();

    return 0;
}

1
Pertimbangkan untuk mengkompilasi contoh dengan sakelar apa pun yang Anda perlukan untuk mendapatkan daftar assembler, dan kemudian memperlihatkan pengkode kode yang, memang, kompiler dapat inline fungsi virtual.
Thomas L Holaday

1
Di atas biasanya tidak akan digariskan, karena Anda memanggil fungsi virtual untuk membantu kelas dasar. Meskipun itu hanya tergantung pada seberapa pintar kompiler. Jika itu bisa menunjukkan bahwa itu pTemp->myVirtualFunction()bisa diselesaikan sebagai panggilan non-virtual, itu mungkin ada sebaris panggilan itu. Panggilan yang direferensikan ini diuraikan oleh g ++ 3.4.2: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();Kode Anda tidak.
doc

1
Satu hal yang sebenarnya dilakukan gcc adalah membandingkan entri vtable dengan simbol tertentu dan kemudian menggunakan varian inline dalam satu lingkaran jika cocok. Ini sangat berguna jika fungsi inline kosong dan loop dapat dihilangkan dalam kasus ini.
Simon Richter

1
@ doc Kompiler modern berusaha keras untuk menentukan pada waktu kompilasi nilai-nilai pointer yang mungkin. Hanya menggunakan pointer saja tidak cukup untuk mencegah inlining pada tingkat optimasi yang signifikan; GCC bahkan melakukan penyederhanaan pada optimisasi nol!
curiousguy

Jawaban:


153

Fungsi virtual kadang-kadang dapat diuraikan. Kutipan dari faq C ++ yang unggul :

"Satu-satunya waktu panggilan virtual sebaris dapat digarisbawahi adalah ketika kompiler mengetahui" kelas yang tepat "dari objek yang merupakan target dari panggilan fungsi virtual. Ini dapat terjadi hanya ketika kompiler memiliki objek aktual daripada pointer atau referensi ke suatu objek. Yaitu, baik dengan objek lokal, objek global / statis, atau objek yang sepenuhnya terkandung di dalam komposit. "


7
Benar, tetapi perlu diingat bahwa kompiler bebas untuk mengabaikan specifier sebaris bahkan jika panggilan dapat diselesaikan pada waktu kompilasi dan dapat digariskan.
sharptooth

6
Situasi lain ketika saya pikir inlining dapat terjadi adalah ketika Anda akan memanggil metode misalnya sebagai ini-> Temp :: myVirtualFunction () - invokation seperti melompati resolusi tabel virtual dan fungsi harus diuraikan tanpa masalah - mengapa dan jika Anda ' d ingin melakukannya adalah topik yang lain :)
RnR

5
@RnR. Tidak perlu memiliki 'ini->', cukup menggunakan nama yang memenuhi syarat sudah cukup. Dan perilaku ini terjadi untuk destruktor, konstruktor, dan secara umum untuk operator penugasan (lihat jawaban saya).
Richard Corden

2
sharptooth - true, tetapi AFAIK ini berlaku untuk semua fungsi inline, bukan hanya fungsi inline virtual.
Colen

2
void f (const Base & lhs, const Base & rhs) {} ------ Dalam implementasi fungsi, Anda tidak pernah tahu apa yang ditunjukkan lhs dan rhs hingga runtime.
Baiyan Huang

72

C ++ 11 telah ditambahkan final. Ini mengubah jawaban yang diterima: tidak lagi perlu untuk mengetahui kelas yang tepat dari objek, itu cukup untuk mengetahui objek memiliki setidaknya tipe kelas di mana fungsi tersebut dinyatakan final:

class A { 
  virtual void foo();
};
class B : public A {
  inline virtual void foo() final { } 
};
class C : public B
{
};

void bar(B const& b) {
  A const& a = b; // Allowed, every B is an A.
  a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C.
}

Tidak dapat memasukkannya di VS 2017.
Yola

1
Saya tidak berpikir itu bekerja seperti ini. Doa foo () melalui pointer / referensi tipe A tidak pernah bisa digarisbawahi. Memanggil b.foo () harus memungkinkan inlining. Kecuali Anda menyarankan bahwa kompiler sudah tahu ini adalah tipe B karena ia mengetahui baris sebelumnya. Tapi itu bukan penggunaan khas.
Jeffrey Faust

Sebagai contoh, bandingkan kode yang dibuat untuk bar dan bas di sini: godbolt.org/g/xy3rNh
Jeffrey Faust

@ JeffreyFaust Tidak ada alasan bahwa informasi tidak boleh disebarkan, bukan? Dan iccsepertinya melakukannya, sesuai tautan itu.
Alexey Romanov

@AlexeyRomanov Compiler memiliki kebebasan untuk mengoptimalkan di luar standar, dan yang pasti dilakukan! Untuk kasus sederhana seperti di atas, kompiler dapat mengetahui tipe dan melakukan optimasi ini. Hal-hal yang jarang ini sesederhana ini, dan itu tidak khas untuk dapat menentukan tipe aktual dari variabel polimorfik pada waktu kompilasi. Saya pikir OP peduli 'secara umum' dan bukan untuk kasus-kasus khusus ini.
Jeffrey Faust

37

Ada satu kategori fungsi virtual di mana masih masuk akal untuk memilikinya inline. Pertimbangkan kasus berikut:

class Base {
public:
  inline virtual ~Base () { }
};

class Derived1 : public Base {
  inline virtual ~Derived1 () { } // Implicitly calls Base::~Base ();
};

class Derived2 : public Derived1 {
  inline virtual ~Derived2 () { } // Implicitly calls Derived1::~Derived1 ();
};

void foo (Base * base) {
  delete base;             // Virtual call
}

Panggilan untuk menghapus 'basis', akan melakukan panggilan virtual untuk memanggil destruktor kelas turunan yang benar, panggilan ini tidak inline. Namun karena setiap destructor menyebutnya induk destructor (yang dalam kasus ini kosong), kompilator dapat inline mereka panggilan, karena mereka tidak memanggil fungsi kelas dasar virtual.

Prinsip yang sama ada untuk konstruktor kelas dasar atau untuk setiap set fungsi di mana implementasi turunan juga memanggil implementasi kelas dasar.


23
Orang harus sadar bahwa kawat gigi kosong tidak selalu berarti destruktor tidak melakukan apa-apa. Destructors default-destruct setiap objek anggota di kelas, jadi jika Anda memiliki beberapa vektor di kelas dasar yang bisa jadi cukup banyak pekerjaan di kawat gigi kosong itu!
Philip

14

Saya telah melihat kompiler yang tidak memancarkan v-table jika tidak ada fungsi non-inline sama sekali ada (dan didefinisikan dalam satu file implementasi, bukan header). Mereka akan melempar kesalahan seperti missing vtable-for-class-Aatau sesuatu yang serupa, dan Anda akan bingung sekali, seperti saya.

Memang, itu tidak sesuai dengan Standar, tetapi itu terjadi jadi pertimbangkan menempatkan setidaknya satu fungsi virtual tidak di header (jika hanya destruktor virtual), sehingga kompiler dapat memancarkan tabel untuk kelas di tempat itu. Saya tahu ini terjadi pada beberapa versigcc .

Seperti yang disebutkan seseorang, fungsi virtual sebaris kadang-kadang bisa bermanfaat , tetapi tentu saja paling sering Anda akan menggunakannya ketika Anda tidak tahu jenis objek yang dinamis, karena itulah alasan utama virtualdi tempat pertama.

Namun kompiler tidak dapat sepenuhnya diabaikan inline. Ini memiliki semantik lain selain mempercepat panggilan fungsi. The inline implisit definisi di kelas adalah mekanisme yang memungkinkan Anda untuk menempatkan definisi ke header: Hanya inlinefungsi dapat didefinisikan beberapa kali sepanjang seluruh program tanpa melanggar aturan. Pada akhirnya, itu berlaku karena Anda hanya akan mendefinisikannya sekali dalam seluruh program, meskipun Anda memasukkan header beberapa kali ke dalam file yang berbeda yang dihubungkan bersama.


11

Sebenarnya fungsi virtual selalu dapat digarisbawahi , asalkan keduanya secara statis dihubungkan bersama: misalkan kita memiliki kelas abstrak Base dengan fungsi virtual Fdan kelas turunan Derived1dan Derived2:

class Base {
  virtual void F() = 0;
};

class Derived1 : public Base {
  virtual void F();
};

class Derived2 : public Base {
  virtual void F();
};

Panggilan hipotetis b->F();(dengan btipe Base*) jelas virtual. Tetapi Anda (atau kompiler ...) dapat menulis ulang seperti itu (misalkan typeofadalah typeidfungsi- like yang mengembalikan nilai yang dapat digunakan dalam a switch)

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // static, inlineable call
  case Derived2: b->Derived2::F(); break; // static, inlineable call
  case Base:     assert(!"pure virtual function call!");
  default:       b->F(); break; // virtual call (dyn-loaded code)
}

sementara kita masih membutuhkan RTTI untuk itu typeof, panggilan secara efektif dapat digarisbawahi oleh, pada dasarnya, menanamkan vtable di dalam aliran instruksi dan mengkhususkan panggilan untuk semua kelas yang terlibat. Ini dapat digeneralisasikan dengan mengkhususkan hanya beberapa kelas (katakan saja, adil Derived1):

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // hot path
  default:       b->F(); break; // default virtual call, cold path
}

Apakah mereka penyusun yang melakukan ini? Atau ini hanya spekulasi? Maaf jika saya terlalu skeptis, tetapi nada Anda dalam deskripsi di atas terdengar seperti - "mereka benar-benar bisa melakukan ini!", Yang berbeda dari "beberapa kompiler melakukan ini".
Alex Meiburg

Ya, Graal melakukan inlining polimorfik (juga untuk bitcode LLVM melalui Sulong)
CAFxX


3

inline benar-benar tidak melakukan apa-apa - itu adalah petunjuk. Kompilator mungkin mengabaikannya atau mungkin inline acara panggilan tanpa inline jika melihat implementasi dan menyukai ide ini. Jika kejelasan kode dipertaruhkan inline harus dihapus.


2
Untuk kompiler yang beroperasi pada TU tunggal saja, mereka hanya bisa sebaris fungsi yang mereka miliki untuk definisi. Suatu fungsi hanya dapat didefinisikan dalam banyak TU jika Anda membuatnya inline. 'inline' lebih dari sekedar petunjuk dan dapat memiliki peningkatan kinerja yang dramatis untuk build g ++ / makefile.
Richard Corden

3

Fungsi Virtual yang dideklarasikan sebaris diuraikan saat dipanggil melalui objek dan diabaikan ketika dipanggil melalui pointer atau referensi.


1

Dengan kompiler modern, tidak ada salahnya untuk inlibe mereka. Beberapa combo compiler / linker kuno mungkin telah membuat beberapa vtables, tapi saya tidak percaya itu adalah masalah lagi.


1

Kompiler hanya dapat inline fungsi ketika panggilan dapat diselesaikan dengan jelas pada waktu kompilasi.

Fungsi virtual, bagaimanapun diselesaikan pada saat runtime, dan karenanya kompiler tidak dapat inline panggilan, karena pada kompilasi ketik tipe dinamis (dan karenanya implementasi fungsi yang akan dipanggil) tidak dapat ditentukan.


1
Ketika Anda memanggil metode kelas dasar dari kelas yang sama atau berasal panggilan adalah jelas dan non-virtual
sharptooth

1
@sharptooth: tetapi kemudian itu akan menjadi metode inline non-virtual. Kompiler dapat menampilkan fungsi yang tidak Anda tanyakan, dan mungkin lebih tahu kapan harus inline atau tidak. Biarkan itu yang memutuskan.
David RodrĂ­guez - dribeas

1
@ Dribeas: Ya, itulah yang saya bicarakan. Saya hanya keberatan dengan pernyataan bahwa finctions virtual diselesaikan pada saat runtime - ini benar hanya ketika panggilan dilakukan secara virtual, bukan untuk kelas yang tepat.
sharptooth

Saya percaya itu omong kosong. Fungsi apa pun selalu dapat digarisbawahi, tidak peduli seberapa besar itu atau apakah itu virtual atau tidak. Itu tergantung pada bagaimana kompiler ditulis. Jika Anda tidak setuju, maka saya berharap bahwa kompiler Anda tidak dapat menghasilkan kode juga. Yaitu: Kompilator dapat menyertakan kode yang pada saat runtime menguji kondisi yang tidak dapat diselesaikan pada waktu kompilasi. Ini seperti kompiler modern yang dapat menyelesaikan nilai konstan / mengurangi ekspresi nummerik pada waktu kompilasi. Jika suatu fungsi / metode tidak digarisbawahi, itu tidak berarti tidak dapat digarisbawahi.

1

Dalam kasus-kasus di mana pemanggilan fungsi tidak ambigu dan fungsi sebagai kandidat yang cocok untuk inlining, kompiler cukup cerdas untuk tetap memasukkan kode.

Sisa waktu "virtual sebaris" adalah omong kosong, dan memang beberapa kompiler tidak akan mengkompilasi kode itu.


Versi g ++ mana yang tidak dapat mengompilasi virtual inline?
Thomas L Holaday

Hm 4.1.1 yang saya miliki di sini sekarang tampak bahagia. Saya pertama kali mengalami masalah dengan basis kode ini menggunakan 4.0.x. Kira info saya kedaluwarsa, diedit.
moonshadow

0

Masuk akal untuk membuat fungsi virtual dan kemudian memanggilnya pada objek daripada referensi atau pointer. Scott Meyer merekomendasikan, dalam bukunya "efektif c ++", untuk tidak pernah mendefinisikan kembali fungsi non-virtual yang diwarisi. Itu masuk akal, karena ketika Anda membuat kelas dengan fungsi non-virtual dan mendefinisikan kembali fungsi dalam kelas turunan, Anda mungkin yakin untuk menggunakannya dengan benar sendiri, tetapi Anda tidak bisa memastikan orang lain akan menggunakannya dengan benar. Juga, Anda di kemudian hari dapat menggunakannya secara salah sendiri. Jadi, jika Anda membuat fungsi di kelas dasar dan Anda ingin dapat didefinisikan ulang, Anda harus membuatnya virtual. Jika masuk akal untuk membuat fungsi virtual dan memanggilnya pada objek, masuk akal juga untuk menyelaraskannya.


0

Sebenarnya dalam beberapa kasus menambahkan "inline" ke virtual final override dapat membuat kode Anda tidak dikompilasi sehingga kadang-kadang ada perbedaan (setidaknya di bawah kompiler VS2017s)!

Sebenarnya saya sedang melakukan fungsi override akhir inline virtual dalam VS2017 menambahkan c ++ 17 standar untuk mengkompilasi dan menghubungkan dan untuk beberapa alasan gagal ketika saya menggunakan dua proyek.

Saya memiliki proyek pengujian dan implementasi DLL yang saya uji unit. Dalam proyek pengujian saya memiliki file "linker_includes.cpp" yang #include file * .cpp dari proyek lain yang diperlukan. Saya tahu ... Saya tahu saya dapat mengatur msbuild untuk menggunakan file objek dari DLL, tetapi harap diingat bahwa ini adalah solusi spesifik microsoft sementara menyertakan file cpp tidak terkait dengan build-system dan jauh lebih mudah untuk versi file cpp daripada file xml dan pengaturan proyek dan ...

Yang menarik adalah saya terus mendapatkan kesalahan linker dari proyek pengujian. Bahkan jika saya menambahkan definisi fungsi yang hilang oleh copy paste dan tidak melalui include! Sangat aneh. Proyek lain telah dibangun dan tidak ada hubungan antara keduanya selain menandai referensi proyek sehingga ada pesanan pembangunan untuk memastikan keduanya selalu dibangun ...

Saya pikir itu adalah semacam bug di kompiler. Saya tidak tahu apakah itu ada di kompiler yang dikirim dengan VS2020, karena saya menggunakan versi yang lebih lama karena beberapa SDK hanya berfungsi dengan benar :-(

Saya hanya ingin menambahkan bahwa tidak hanya menandai mereka sebagai inline dapat berarti sesuatu, tetapi bahkan mungkin membuat kode Anda tidak membangun dalam beberapa keadaan langka! Ini aneh, namun baik untuk diketahui.

PS .: Kode yang sedang saya kerjakan terkait dengan grafik komputer, jadi saya lebih suka inlining dan itulah sebabnya saya menggunakan final dan inline. Saya menyimpan specifier terakhir untuk berharap rilis rilis cukup pintar untuk membangun DLL dengan menggarisbawahi bahkan tanpa saya secara langsung mengisyaratkan jadi ...

PS (Linux) .: Saya berharap hal yang sama tidak terjadi di gcc atau dentang karena saya secara rutin melakukan hal-hal semacam ini. Saya tidak yakin dari mana masalah ini berasal ... Saya lebih suka melakukan c ++ di Linux atau setidaknya dengan beberapa gcc, tetapi terkadang proyek berbeda dalam kebutuhan.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.