Satır içi sanal işlevler gerçekten mantıklı değil mi?


173

Sanal işlevler satır içinde olması gerektiğini söyleyerek bir kod inceleme yorum aldığımda bu soruyu aldım.

Satır içi sanal işlevlerin, işlevlerin doğrudan nesneler üzerinde çağrıldığı senaryolarda kullanışlı olabileceğini düşündüm. Ama aklıma gelen karşı argüman - neden sanal tanımlamak ve sonra yöntemleri çağırmak için nesneleri kullanmak istesin ki?

Neredeyse hiç genişletilmedikleri için satır içi sanal işlevleri kullanmamak en iyisidir?

Analiz için kullandığım kod snippet'i:

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
Bir montajcı listesi almak için ihtiyacınız olan anahtarları içeren bir örnek derlemeyi ve daha sonra derleyicinin sanal işlevleri satır içine alabileceğini kod inceleyicisini göstermeyi düşünün.
Thomas L Holaday

1
Yukarıdakiler genellikle satır içine alınmayacaktır, çünkü temel sınıf yardımı için sanal işlevi çağırıyorsunuz. Her ne kadar sadece derleyicinin ne kadar akıllı olduğuna bağlı. Bunun pTemp->myVirtualFunction()sanal olmayan çağrı olarak çözülebileceğine işaret edebiliyorsa , bu çağrıyı yerinde yapabilir. Başvurulan bu çağrı g ++ 3.4.2 ile işaretlenmiştir: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();Kodunuz değildir.
doc

1
Gcc'nin gerçekte yaptığı bir şey, vtable girişini belirli bir simgeyle karşılaştırmak ve daha sonra eşleşirse bir döngüde satır içi bir varyant kullanmaktır. Bu özellikle, satır içi işlev boşsa ve döngü bu durumda ortadan kaldırılabiliyorsa yararlıdır.
Simon Richter

1
@doc Modern derleyici derleme sırasında olası işaretçilerin değerlerini belirlemek için çok çalışmaktadır. Yalnızca bir işaretçi kullanmak, herhangi bir önemli optimizasyon düzeyinde satır içi çizgiyi önlemek için yeterli değildir; GCC optimizasyon sıfırında bile basitleştirmeler yapar!
curiousguy

Yanıtlar:


153

Sanal işlevler bazen satır içine alınabilir. Mükemmel C ++ SSS'den bir alıntı :

"Satır içi sanal çağrının satır içine alınabileceği tek zaman, derleyicinin sanal işlev çağrısının hedefi olan nesnenin" tam sınıfını "bilmesi durumudur. Bu, yalnızca derleyicinin işaretçi yerine gerçek bir nesnesi olduğunda veya yani bir yerel nesne, bir genel / statik nesne ya da bir bileşik içinde tam olarak kaplanmış bir nesne ile. "


7
Doğru, ancak çağrı derleme zamanında çözümlenebilse ve satır içine alınabilse bile, derleyicinin satır içi belirleyiciyi yok saymakta serbest olduğunu hatırlamakta fayda var.
sharptooth

6
Inlining olabileceğini düşündüğümde başka bir durum, örneğin bu-> Temp :: myVirtualFunction () gibi yöntemi çağırdığınızda - böyle bir çağrı sanal tablo çözünürlüğünü atlar ve işlev sorunsuz bir şekilde satır içine alınmalıdır - neden ve eğer d başka bir konu yapmak istiyorum :)
RnR

5
@RnR. 'This->' ye sahip olmak gerekli değildir, sadece nitelikli ismi kullanmak yeterlidir. Ve bu davranış yıkıcılar, inşaatçılar ve genel olarak atama operatörleri için gerçekleşir (cevabıma bakın).
Richard Corden

2
sharptooth - doğru, ancak AFAIK bu yalnızca sanal satır içi işlevler için değil, tüm satır içi işlevler için geçerlidir.
Colen

2
void f (const Base & lhs, const Base & rhs) {} ------ Fonksiyonun uygulanmasında, lhs ve rhs'nin çalışma zamanına kadar neyi işaret ettiğini asla bilemezsiniz.
Baiyan Huang

72

C ++ 11 eklendi final. Bu, kabul edilen cevabı değiştirir: artık nesnenin tam sınıfını bilmek gerekli değildir, nesnenin en azından işlevin son olarak bildirildiği sınıf türüne sahip olduğunu bilmek yeterlidir:

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.
}

VS 2017'de satır içi yapamadı.
Yola

1
Bu şekilde çalıştığını sanmıyorum. Bir işaretçi / tip A referansı ile foo () işlevinin çağrılması hiçbir zaman satır içine alınamaz. B.foo () işlevinin çağrılması satırsonuna izin vermelidir. Derleyicinin zaten bir B tipi olduğunu bilmediğiniz sürece, önceki satırın farkında olduğu için. Ancak bu tipik kullanım değildir.
Jeffrey Faust

Örneğin, bar ve bas için oluşturulan kodu burada karşılaştırın: godbolt.org/g/xy3rNh
Jeffrey Faust

@JeffreyFaust Bilginin yayılmaması için hiçbir neden yok, değil mi? Ve iccbu bağlantıya göre bunu yapıyor gibi görünüyor.
Alexey Romanov

@AlexeyRomanov Derleyicileri standartların ötesinde optimizasyon özgürlüğüne sahiptir ve kesinlikle! Yukarıdaki gibi basit durumlar için, derleyici türü bilebilir ve bu optimizasyonu yapabilir. İşler nadiren bu kadar basittir ve derleme zamanında bir polimorfik değişkenin gerçek tipini belirleyebilmek tipik değildir. Bence OP bu özel durumlar için değil, 'genel olarak' önem veriyor.
Jeffrey Faust

37

Satır içi olmalarının hala mantıklı olduğu bir sanal işlev kategorisi vardır. Aşağıdaki durumu düşünün:

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
}

'Base' silme çağrısı, doğru türetilmiş sınıf yıkıcıyı çağırmak için sanal bir çağrı gerçekleştirecektir, bu çağrı satır içi değildir. Bununla birlikte, her yıkıcı kendi üst yıkıcısını çağırdığı için (bu durumlarda boştur), derleyici bu çağrıları satır içine alabilir , çünkü temel sınıf işlevlerini sanal olarak çağırmazlar.

Aynı prensip, temel sınıf yapıcıları veya türetilmiş uygulamanın temel sınıflar uygulamasını da çağırdığı herhangi bir işlev kümesi için de geçerlidir.


23
Ancak boş parantezlerin her zaman yıkıcı hiçbir şey yapmadığı anlamına gelmediğinin farkında olunmalıdır. Yıkıcılar, sınıftaki her üye nesneyi varsayılan olarak yok eder, bu nedenle temel sınıfta bu boş parantezlerde oldukça fazla iş olabilecek birkaç vektörünüz varsa!
Philip

14

Hiçbir satır içi işlevi yoksa (ve sonra bir başlık yerine bir uygulama dosyasında tanımlanmış) herhangi bir v-tablo yaymayan derleyiciler gördüm. Onlar gibi hatalar missing vtable-for-class-Aya da benzer şeyler fırlatacaklardı ve benim gibi cehennem gibi kafan karışacaktı.

Aslında, bu Standart ile uyumlu değildir, ancak bu durumda, en azından bir sanal işlevi başlıkta değil (yalnızca sanal yıkıcıysa) koymayı düşünün, böylece derleyici o yerdeki sınıf için bir vtable yayabilir. Bunun bazı sürümlerinde olduğunu biliyorumgcc .

Birisi de belirtildiği gibi, satır içi sanal fonksiyonlar bir yararı olabilir bazen ama ne zaman tabii en sık kullanırız değil nesnenin dinamik türünü biliyorum tüm sebebi buydu çünkü, virtualilk etapta.

Ancak derleyici tamamen görmezden gelemez inline. Bir işlev çağrısını hızlandırmanın dışında başka anlambilime sahiptir. Örtük inline sınıf tanımları için başlığın içine tanımını koymak sağlayan mekanizmadır: Sadece inlinefonksiyonları ihlali herhangi bir kural olmadan tüm program boyunca birden çok kez tanımlanabilir. Sonunda, başlığı birbirine bağlı farklı dosyalara birden çok kez eklemiş olsanız bile, programın tamamında yalnızca bir kez tanımlamış olduğunuz gibi davranır.


11

Aslında, sanal işlevler statik olarak birbirine bağlı oldukları sürece her zaman satır içine alınabilir : Base sanal bir işleve Fve türetilmiş sınıflara sahip soyut bir sınıfımız olduğunu varsayalım Derived1ve Derived2:

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

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

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

Hipotetik bir çağrı b->F();( btür ile Base*) açıkça sanaldır. Ama (veya derleyici ...) o kadar seviyorum yazabilirsiniz (varsayalım typeofbir olduğunu typeidbenzeri işlevi olduğunu döndürür bir de kullanılabilecek bir değer 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)
}

RTTI için hala ihtiyacımız olsa da typeof, çağrı etkili bir şekilde, vtable'ı talimat akışının içine gömerek ve çağrıyı tüm ilgili sınıflar için uzmanlaştırarak etkili bir şekilde sıralanabilir. Bu, sadece birkaç sınıfın uzmanlaştırılmasıyla da genelleştirilebilir (örneğin, sadece Derived1):

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

Bunu yapan herhangi bir derleyici var mı? Yoksa bu sadece bir spekülasyon mu? Çok şüpheci olduğum için üzgünüm, ancak yukarıdaki açıklamadaki tonunuz bir çeşit gibi geliyor - "tamamen bunu yapabilirler!".
Alex Meiburg

Evet, Graal polimorfik satır içi (Sulong üzerinden LLVM bit kodu için) yapar
CAFxX


3

inline gerçekten hiçbir şey yapmaz - bu bir ipucu. Derleyici, bunu görmezden gelebilir veya uygulamayı görür ve bu fikri beğenirse, satır içi olmadan bir çağrı olayı satır içi yapabilir. Kod netliği söz konusu olduğunda satır içi çıkarılmalıdır.


2
Yalnızca tek TU'larda çalışan derleyiciler için, yalnızca tanımına sahip oldukları dolaylı işlevler için satır içi işlevler kullanılabilir. Bir işlev yalnızca satır içi yaparsanız birden çok TU'da tanımlanabilir. 'satır içi' bir ipucundan daha fazlasıdır ve bir g ++ / makefile derlemesi için çarpıcı bir performans geliştirmesine sahip olabilir.
Richard Corden

3

Satır içi bildirilen Sanal işlevler, nesneler üzerinden çağrıldığında satır içi ve işaretçi veya referanslar ile çağrıldığında yok sayılır.


1

Modern derleyicilerle, onları inlabe etmek zarar vermez. Bazı eski derleyici / bağlayıcı kombinasyonları birden fazla vtable oluşturmuş olabilir, ancak artık bunun bir sorun olduğuna inanmıyorum.


1

Bir derleyici yalnızca çağrı derleme zamanında net bir şekilde çözülebildiğinde bir işlevi satır içine alabilir.

Bununla birlikte, sanal işlevler çalışma zamanında çözümlenir ve bu nedenle derleyici çağrıyı satır içine alamaz, çünkü derleme türünde dinamik tür (ve dolayısıyla çağrılacak işlev uygulaması) belirlenemez.


1
Aynı veya türetilmiş bir sınıftan bir temel sınıf yöntemi çağırdığınızda, çağrı açık ve sanal olmayan
sharptooth 11

1
@sharptooth: ama o zaman sanal olmayan bir satır içi yöntem olurdu. Derleyici, istemediğiniz satır içi işlevleri yapabilir ve satır içi veya satır içi yapmamanızı muhtemelen daha iyi bilir. Karar versin.
David Rodríguez - dribeas

1
@ dribeas: Evet, tam olarak bundan bahsediyorum. Sadece sanal kurguların çalışma zamanında çözüldüğüne dair ifadeye itiraz ettim - bu sadece çağrı tam olarak sınıf için değil, sanal olarak yapıldığında doğrudur.
sharptooth

Bunun saçmalık olduğuna inanıyorum. Ne kadar büyük olursa olsun ya da sanal olsun ya da olmasın, herhangi bir işlev her zaman satır içine alınabilir. Derleyicinin nasıl yazıldığına bağlıdır. Kabul etmiyorsanız, derleyicinizin eğik olmayan kodu da üretememesini beklerim. Yani: Derleyici, derleme zamanında çözemediği koşullar için çalışma zamanı testlerinde kod içerebilir. Modern derleyiciler sabit değerleri çözebilir / derleme zamanında sayısal ifadeleri azaltabilir. Bir işlev / yöntem satır içi değilse, satır içi kullanılamayacağı anlamına gelmez.

1

İşlev çağrısının açık olmadığı ve işlevin satır içine almak için uygun bir aday olduğu durumlarda, derleyici kodu yine de satır içine alacak kadar akıllıdır.

Geri kalan "satır içi sanal" saçmalıktır ve aslında bazı derleyiciler bu kodu derlemez.


Hangi g ++ sürümü satır içi sanalları derlemez?
Thomas L Holaday

Hm. Burada sahip olduğum 4.1.1 şimdi mutlu görünüyor. İlk olarak 4.0.x kullanarak bu kod temeli ile ilgili sorunlarla karşılaştım. Sanırım bilgilerim güncel değil, düzenlendi.
moonshadow

0

Sanal işlevler yapmak ve sonra bunları referanslar veya işaretçiler yerine nesneler üzerinde çağırmak mantıklıdır. Scott Meyer, "etkili c ++" adlı kitabında, kalıtsal bir sanal olmayan işlevi asla yeniden tanımlamamayı önerir. Bu mantıklı değildir, çünkü sanal olmayan bir işleve sahip bir sınıf oluşturduğunuzda ve türetilmiş bir sınıfta işlevi yeniden tanımladığınızda, bunu kendiniz doğru kullandığınızdan emin olabilirsiniz, ancak başkalarının doğru şekilde kullanacağından emin olamazsınız. Ayrıca, daha sonraki bir tarihte yanlış yoruself kullanabilirsiniz. Bu nedenle, bir temel sınıfta bir işlev yaparsanız ve işlevin yeniden yapılandırılabilir olmasını istiyorsanız, onu sanal yapmalısınız. Sanal işlevler yapmak ve bunları nesneler üzerinde çağırmak mantıklıysa, bunları satır içi yapmak da mantıklıdır.


0

Aslında bazı durumlarda sanal bir son geçersiz kılma işlemine "satır içi" eklemek, kodunuzun derlenmemesini sağlayabilir, bu nedenle bazen bir fark olabilir (en azından VS2017s derleyicisi altında)!

Aslında VS2017'de derlemek ve bağlantı kurmak için c ++ 17 standardını ekleyerek sanal bir satır içi son geçersiz kılma işlevi yapıyordum ve iki projeyi kullanırken bir nedenden dolayı başarısız oldu.

Ben bir test projesi ve birim test olduğum bir uygulama DLL vardı. Test projesinde, gerekli diğer projedeki * .cpp dosyalarını içeren # "linker_includes.cpp" dosyası var. Biliyorum ... msbuild'i DLL'den nesne dosyalarını kullanacak şekilde ayarlayabileceğimi biliyorum, ancak cpp dosyalarını dahil etmekle ilgili bir microsoft'a özgü çözüm olduğunu ve sürümün çok daha kolay olduğunu unutmayın. xml dosyaları ve proje ayarları ve benzeri daha bir cpp dosyası ...

İlginç olan, test projesinden sürekli olarak linker hatası alıyordum. Kayıp fonksiyonların tanımını kopyala yapıştır ile eklemiş olsam da dahil et! Çok garip. Diğer proje inşa etti ve ikisi arasında bir proje referansı işaretlemek dışında bir bağlantı yok, bu yüzden her ikisinin de her zaman inşa edildiğinden emin olmak için bir inşa emri var ...

Ben derleyici bir tür hata olduğunu düşünüyorum. VS2020 ile birlikte gelen derleyicide olup olmadığına dair hiçbir fikrim yok, çünkü bazı SDK'ların yalnızca düzgün çalıştığı için eski bir sürüm kullanıyorum :-(

Sadece bunları satır içi olarak işaretlemenin bir şey ifade edebileceğini değil, hatta kodunuzun bazı nadir durumlarda oluşturulmamasını sağlayabileceğini de eklemek istedim! Bu garip ama bilmek güzel.

PS .: Üzerinde çalıştığım kod inlineing tercih bilgisayar grafikleri ile ilgili olduğunu ve bu yüzden hem final hem de satır içi kullandım. Ben serbest bırakma bile doğrudan ipucu olmadan bindirerek DLL oluşturmak için yeterince akıllı olduğunu umuyoruz son belirleyici tuttu ...

PS (Linux) .: Bu tür şeyleri rutin olarak yaptığım gibi gcc veya clang'da da aynı şeylerin olmasını beklemiyorum. Bu sorunun nereden geldiğinden emin değilim ... Linux'ta veya en azından biraz gcc ile c ++ yapmayı tercih ediyorum, ancak bazen proje ihtiyaçları farklı.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.