Là các chức năng ảo nội tuyến thực sự là một vô nghĩa?


172

Tôi nhận được câu hỏi này khi nhận được một nhận xét đánh giá mã nói rằng các hàm ảo không cần phải là nội tuyến.

Tôi nghĩ rằng các hàm ảo nội tuyến có thể có ích trong các tình huống trong đó các hàm được gọi trực tiếp trên các đối tượng. Nhưng đối số xuất hiện trong đầu tôi là - tại sao người ta lại muốn định nghĩa ảo và sau đó sử dụng các đối tượng để gọi các phương thức?

Có phải tốt nhất là không sử dụng các chức năng ảo nội tuyến, vì dù sao chúng gần như không bao giờ được mở rộng?

Đoạn mã tôi đã sử dụng để phân tích:

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
Xem xét việc biên dịch một ví dụ với bất kỳ công tắc nào bạn cần để có được danh sách trình biên dịch chương trình, và sau đó hiển thị trình xem xét mã rằng, thực sự, trình biên dịch có thể thực hiện các hàm ảo.
Thomas L Holaday

1
Ở trên thường sẽ không được nội tuyến, bởi vì bạn đang gọi hàm ảo để hỗ trợ lớp cơ sở. Mặc dù nó chỉ phụ thuộc vào mức độ thông minh của trình biên dịch. Nếu nó có thể chỉ ra rằng pTemp->myVirtualFunction()có thể được giải quyết dưới dạng cuộc gọi không ảo, thì nó có thể có cuộc gọi nội tuyến đó. Cuộc gọi được tham chiếu này được nội tuyến bởi g ++ 3.4.2: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();Mã của bạn thì không.
doc

1
Một điều gcc thực sự làm là so sánh mục nhập vtable với một biểu tượng cụ thể và sau đó sử dụng một biến thể nội tuyến trong một vòng lặp nếu nó phù hợp. Điều này đặc biệt hữu ích nếu hàm nội tuyến trống và vòng lặp có thể được loại bỏ trong trường hợp này.
Simon Richter

1
@doc Trình biên dịch hiện đại cố gắng hết sức để xác định tại thời điểm biên dịch các giá trị có thể có của con trỏ. Chỉ sử dụng một con trỏ là không đủ để ngăn chặn nội tuyến ở bất kỳ mức tối ưu hóa đáng kể nào; GCC thậm chí thực hiện đơn giản hóa ở mức tối ưu hóa bằng không!
tò mò

Câu trả lời:


153

Các chức năng ảo đôi khi có thể được nội tuyến. Một đoạn trích từ faq C ++ xuất sắc :

"Lần duy nhất một cuộc gọi ảo nội tuyến có thể được nội tuyến là khi trình biên dịch biết" lớp chính xác "của đối tượng là mục tiêu của cuộc gọi hàm ảo. Điều này chỉ có thể xảy ra khi trình biên dịch có một đối tượng thực tế chứ không phải là một con trỏ hoặc tham chiếu đến một đối tượng. Tức là, với một đối tượng cục bộ, một đối tượng toàn cục / tĩnh hoặc một đối tượng được chứa đầy đủ bên trong một hỗn hợp. "


7
Đúng, nhưng đáng nhớ là trình biên dịch có thể bỏ qua trình xác định nội tuyến ngay cả khi cuộc gọi có thể được giải quyết tại thời điểm biên dịch và có thể được nội tuyến.
sharptooth

6
Tình huống thuận lợi hơn khi tôi nghĩ rằng nội tuyến có thể xảy ra là khi bạn gọi phương thức đó là ví dụ này-> Temp :: myVirtualFunction () - việc gọi như vậy bỏ qua độ phân giải bảng ảo và chức năng sẽ được nội tuyến mà không gặp sự cố - tại sao và nếu bạn ' Tôi muốn làm điều đó là một chủ đề khác :)
RnR

5
@RnR. Không cần thiết phải có 'this->', chỉ cần sử dụng tên đủ điều kiện là đủ. Và hành vi này diễn ra đối với các hàm hủy, hàm tạo và nói chung cho các toán tử gán (xem câu trả lời của tôi).
Richard Corden

2
sharptooth - đúng, nhưng AFAIK điều này đúng với tất cả các hàm nội tuyến, không chỉ các hàm nội tuyến ảo.
Colen

2
void f (const Base & lhs, const Base & rhs) {} ------ Khi thực hiện chức năng, bạn không bao giờ biết lhs và rhs trỏ đến điều gì khi chạy.
Baiyan Huang

72

C ++ 11 đã thêm final. Điều này thay đổi câu trả lời được chấp nhận: không còn cần thiết phải biết chính xác lớp của đối tượng, đủ để biết đối tượng có ít nhất loại lớp trong đó hàm được khai báo cuối cùng:

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

Không thể nội tuyến nó trong VS 2017.
Yola

1
Tôi không nghĩ rằng nó hoạt động theo cách này. Yêu cầu của foo () thông qua một con trỏ / tham chiếu loại A không bao giờ có thể được nội tuyến. Gọi b.foo () sẽ cho phép nội tuyến. Trừ khi bạn đề xuất rằng trình biên dịch đã biết đây là loại B vì nó biết về dòng trước đó. Nhưng đó không phải là cách sử dụng thông thường.
Jeffrey Faust

Ví dụ: so sánh mã được tạo cho thanh và bas ở đây: godbolt.org/g/xy3rNh
Jeffrey Faust

@JeffreyFaust Không có lý do gì mà thông tin không nên được truyền bá, phải không? Và iccdường như để làm điều đó, theo liên kết đó.
Alexey Romanov

Trình biên dịch @AlexeyRomanov có quyền tự do tối ưu hóa vượt quá tiêu chuẩn, và chắc chắn là có! Đối với các trường hợp đơn giản như trên, trình biên dịch có thể biết loại và thực hiện tối ưu hóa này. Mọi thứ hiếm khi đơn giản như vậy và nó không phải là điển hình để có thể xác định loại thực tế của một biến đa hình tại thời điểm biên dịch. Tôi nghĩ OP quan tâm đến 'nói chung' và không dành cho những trường hợp đặc biệt này.
Jeffrey Faust

37

Có một loại chức năng ảo trong đó vẫn có ý nghĩa để có chúng nội tuyến. Hãy xem xét trường hợp sau:

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
}

Cuộc gọi để xóa 'cơ sở', sẽ thực hiện một cuộc gọi ảo để gọi hàm hủy lớp dẫn xuất chính xác, cuộc gọi này không được nội tuyến. Tuy nhiên vì mỗi hàm hủy gọi nó là hàm hủy của cha (trong các trường hợp này là trống), trình biên dịch có thể định tuyến các lệnh đó, vì chúng hầu như không gọi các hàm lớp cơ sở.

Nguyên tắc tương tự tồn tại cho các hàm tạo của lớp cơ sở hoặc cho bất kỳ tập hợp các hàm nào trong đó việc thực hiện dẫn xuất cũng gọi việc thực hiện các lớp cơ sở.


23
Mọi người nên biết rằng mặc dù niềng răng trống không luôn có nghĩa là kẻ hủy diệt không làm gì cả. Các cấu trúc hủy mặc định phá hủy mọi đối tượng thành viên trong lớp, vì vậy nếu bạn có một vài vectơ trong lớp cơ sở có thể có khá nhiều công việc trong các dấu ngoặc rỗng đó!
Philip

14

Tôi đã thấy các trình biên dịch không phát ra bất kỳ bảng v nào nếu không có hàm phi tuyến nào tồn tại (và được xác định trong một tệp thực hiện thay vì tiêu đề sau đó). Họ sẽ ném các lỗi như missing vtable-for-class-Ahoặc một cái gì đó tương tự, và bạn sẽ bị nhầm lẫn như địa ngục.

Thật vậy, điều đó không phù hợp với Tiêu chuẩn, nhưng nó xảy ra vì vậy hãy xem xét đưa ít nhất một hàm ảo không vào tiêu đề (nếu chỉ là hàm hủy ảo), để trình biên dịch có thể phát ra một vtable cho lớp tại vị trí đó. Tôi biết nó xảy ra với một số phiên bản của gcc.

Như ai đó đã đề cập, các hàm ảo nội tuyến đôi khi có thể là một lợi ích , nhưng tất nhiên, thông thường bạn sẽ sử dụng nó khi bạn không biết loại động của đối tượng, vì đó là toàn bộ lý do virtualtại thời điểm đầu tiên.

Trình biên dịch tuy nhiên không thể hoàn toàn bỏ qua inline. Nó có các ngữ nghĩa khác ngoài việc tăng tốc một cuộc gọi chức năng. Nội tuyến ẩn cho các định nghĩa trong lớp là cơ chế cho phép bạn đặt định nghĩa vào tiêu đề: Chỉ các inlinehàm có thể được xác định nhiều lần trong toàn bộ chương trình mà không vi phạm bất kỳ quy tắc nào. Cuối cùng, nó hoạt động như bạn chỉ định nghĩa nó một lần trong toàn bộ chương trình, mặc dù bạn đã bao gồm tiêu đề nhiều lần vào các tệp khác nhau được liên kết với nhau.


11

Chà, thực ra các hàm ảo luôn có thể được nội tuyến , miễn là chúng được liên kết tĩnh với nhau: giả sử chúng ta có một lớp trừu tượng Base với một hàm ảo Fvà các lớp dẫn xuất Derived1Derived2:

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

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

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

Một cuộc gọi giả định b->F();(với bloại Base*) rõ ràng là ảo. Nhưng bạn (hoặc trình biên dịch ...) có thể viết lại như vậy (giả sử typeoflà một typeidhàm giống như trả về một giá trị có thể được sử dụng trong 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)
}

trong khi chúng ta vẫn cần RTTI cho typeof, cuộc gọi có thể được thực hiện một cách hiệu quả bằng cách, về cơ bản, nhúng vtable bên trong luồng lệnh và chuyên gọi cho tất cả các lớp liên quan. Điều này cũng có thể được khái quát bằng cách chỉ chuyên một vài lớp (nói, chỉ Derived1):

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

Họ có bất kỳ trình biên dịch nào làm điều này? Hay đây chỉ là suy đoán? Xin lỗi nếu tôi quá hoài nghi, nhưng giọng điệu của bạn trong phần mô tả ở trên nghe có vẻ giống như - "họ hoàn toàn có thể làm điều này!", Khác với "một số trình biên dịch làm điều này".
Alex Meiburg

Có, Graal thực hiện nội tuyến đa hình (cũng cho mã bit LLVM qua Sulong)
CAFxX


3

nội tuyến thực sự không làm gì cả - đó là một gợi ý. Trình biên dịch có thể bỏ qua nó hoặc nó có thể nội tuyến một sự kiện cuộc gọi mà không có nội tuyến nếu nó thấy việc thực hiện và thích ý tưởng này. Nếu độ rõ của mã bị đe doạ thì nội tuyến sẽ bị xóa.


2
Đối với các trình biên dịch chỉ hoạt động trên các TU đơn, chúng chỉ có thể thực hiện các hàm ngầm định mà chúng có định nghĩa. Một chức năng chỉ có thể được xác định trong nhiều TU nếu bạn đặt nó nội tuyến. 'nội tuyến' không chỉ là một gợi ý và nó có thể cải thiện hiệu suất đáng kể cho bản dựng g ++ / makefile.
Richard Corden

3

Các hàm ảo được khai báo nội tuyến được nội tuyến khi được gọi thông qua các đối tượng và bị bỏ qua khi được gọi thông qua con trỏ hoặc tham chiếu.


1

Với các trình biên dịch hiện đại, nó sẽ không gây hại gì khi xâm nhập vào chúng. Một số combo trình biên dịch / liên kết cổ có thể đã tạo ra nhiều vtables, nhưng tôi không tin đó là vấn đề nữa.


1

Một trình biên dịch chỉ có thể nội tuyến một hàm khi cuộc gọi có thể được giải quyết rõ ràng tại thời điểm biên dịch.

Tuy nhiên, các hàm ảo được giải quyết trong thời gian chạy và do đó trình biên dịch không thể thực hiện cuộc gọi, vì tại kiểu biên dịch, loại động (và do đó thực hiện chức năng được gọi) không thể được xác định.


1
Khi bạn gọi một phương thức lớp cơ sở từ cùng một lớp hoặc dẫn xuất, cuộc gọi không rõ ràng và không ảo
sharptooth

1
@sharptooth: nhưng sau đó nó sẽ là một phương thức nội tuyến không ảo. Trình biên dịch có thể thực hiện các chức năng nội tuyến mà bạn không yêu cầu và có thể biết rõ hơn khi nào nên nội tuyến hay không. Hãy để nó quyết định.
David Rodríguez - dribeas

1
@dribeas: Vâng, đó chính xác là những gì tôi đang nói. Tôi chỉ phản đối tuyên bố rằng các giả tưởng ảo được giải quyết trong thời gian chạy - điều này chỉ đúng khi cuộc gọi được thực hiện hầu như không phải cho lớp chính xác.
sharptooth

Tôi tin rằng điều đó là vô nghĩa. Bất kỳ chức năng nào luôn có thể được nội tuyến, bất kể nó lớn như thế nào hoặc có ảo hay không. Nó phụ thuộc vào cách trình biên dịch đã được viết. Nếu bạn không đồng ý, thì tôi hy vọng rằng trình biên dịch của bạn cũng không thể tạo mã không nội tuyến. Đó là: Trình biên dịch có thể bao gồm mã mà tại các bài kiểm tra thời gian chạy cho các điều kiện mà nó không thể giải quyết tại thời gian biên dịch. Nó giống như các trình biên dịch hiện đại có thể giải quyết các giá trị không đổi / giảm các biểu thức nummeric tại thời gian biên dịch. Nếu một hàm / phương thức không được nội tuyến, điều đó không có nghĩa là nó không thể được nội tuyến.

1

Trong trường hợp lệnh gọi hàm không rõ ràng và hàm là một ứng cử viên phù hợp để đặt nội tuyến, trình biên dịch đủ thông minh để nội tuyến mã bằng mọi cách.

Phần còn lại của thời gian "ảo nội tuyến" là vô nghĩa, và thực sự một số trình biên dịch sẽ không biên dịch mã đó.


Phiên bản nào của g ++ sẽ không biên dịch ảo nội tuyến?
Thomas L Holaday

Hừm. 4.1.1 tôi có ở đây bây giờ dường như là hạnh phúc. Lần đầu tiên tôi gặp vấn đề với cơ sở mã này khi sử dụng 4.0.x. Đoán thông tin của tôi đã hết hạn, chỉnh sửa.
trăng

0

Thật có ý nghĩa để tạo các hàm ảo và sau đó gọi chúng trên các đối tượng thay vì tham chiếu hoặc con trỏ. Scott Meyer khuyến nghị, trong cuốn sách "c ++ hiệu quả" của mình, không bao giờ định nghĩa lại một chức năng phi ảo được thừa hưởng. Điều đó có ý nghĩa, bởi vì khi bạn tạo một lớp có hàm không ảo và xác định lại hàm trong lớp dẫn xuất, bạn có thể chắc chắn sử dụng chính xác nó, nhưng bạn không thể chắc chắn người khác sẽ sử dụng nó một cách chính xác. Ngoài ra, bạn có thể sử dụng nó một cách không chính xác. Vì vậy, nếu bạn tạo một hàm trong một lớp cơ sở và bạn muốn nó có thể được định nghĩa lại, bạn nên biến nó thành ảo. Nếu nó có ý nghĩa để tạo các hàm ảo và gọi chúng trên các đối tượng, thì cũng có nghĩa là nội tuyến chúng.


0

Trên thực tế, trong một số trường hợp, việc thêm "nội tuyến" vào ghi đè cuối cùng ảo có thể làm cho mã của bạn không được biên dịch nên đôi khi có sự khác biệt (ít nhất là trong trình biên dịch VS2017)!

Trên thực tế, tôi đã thực hiện một chức năng ghi đè nội tuyến ảo trong VS2017, thêm tiêu chuẩn c ++ 17 để biên dịch và liên kết và vì một số lý do, nó đã thất bại khi tôi đang sử dụng hai dự án.

Tôi đã có một dự án thử nghiệm và một DLL thực hiện mà tôi đang thử nghiệm đơn vị. Trong dự án thử nghiệm, tôi đang có một tệp "linker_includes.cpp" #inc loại các tệp * .cpp từ dự án khác cần thiết. Tôi biết ... Tôi biết tôi có thể thiết lập msbuild để sử dụng các tệp đối tượng từ DLL, nhưng xin lưu ý rằng đó là một giải pháp cụ thể của microsoft trong khi bao gồm các tệp cpp không liên quan đến hệ thống xây dựng và phiên bản dễ dàng hơn nhiều một tệp cpp hơn các tệp xml và cài đặt dự án và như vậy ...

Điều thú vị là tôi liên tục nhận được lỗi liên kết từ dự án thử nghiệm. Ngay cả khi tôi đã thêm định nghĩa của các chức năng bị thiếu bằng cách sao chép dán và không thông qua bao gồm! Thật ki cục. Dự án khác đã được xây dựng và không có mối liên hệ nào giữa hai dự án ngoài việc đánh dấu một tham chiếu dự án để có một lệnh xây dựng để đảm bảo cả hai luôn được xây dựng ...

Tôi nghĩ rằng đó là một số loại lỗi trong trình biên dịch. Tôi không biết nó có tồn tại trong trình biên dịch được gửi cùng với VS2020 không, vì tôi đang sử dụng phiên bản cũ hơn vì một số SDK chỉ hoạt động đúng với điều đó :-(

Tôi chỉ muốn thêm rằng không chỉ đánh dấu chúng là nội tuyến có thể có nghĩa gì đó, mà thậm chí có thể làm cho mã của bạn không được xây dựng trong một số trường hợp hiếm hoi! Điều này là lạ, nhưng tốt để biết.

PS.: Mã tôi đang làm việc có liên quan đến đồ họa máy tính vì vậy tôi thích nội tuyến hơn và đó là lý do tại sao tôi sử dụng cả cuối cùng và nội tuyến. Tôi đã giữ công cụ xác định cuối cùng để hy vọng bản phát hành đủ thông minh để xây dựng DLL bằng cách nội tuyến ngay cả khi tôi không gợi ý trực tiếp để ...

PS (Linux): Tôi hy vọng điều tương tự không xảy ra trong gcc hoặc clang khi tôi thường xuyên sử dụng để làm những việc này. Tôi không chắc vấn đề này đến từ đâu ... Tôi thích làm c ++ trên Linux hoặc ít nhất là với một số gcc, nhưng đôi khi dự án lại có nhu cầu khác nhau.

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.