“Nội tuyến” không có “tĩnh” hoặc “ngoại” có bao giờ hữu ích trong C99 không?


95

Khi tôi cố gắng xây dựng mã này

inline void f() {}

int main()
{
    f();
}

sử dụng dòng lệnh

gcc -std=c99 -o a a.c

Tôi gặp lỗi trình liên kết (tham chiếu không xác định đến f). Lỗi sẽ biến mất nếu tôi sử dụng static inlinehoặc extern inlinethay vì chỉ inline, hoặc nếu tôi biên dịch với -O(vì vậy hàm thực sự được nội tuyến).

Hành vi này dường như được định nghĩa trong đoạn 6.7.4 (6) của tiêu chuẩn C99:

Nếu tất cả các khai báo phạm vi tệp cho một hàm trong một đơn vị dịch bao gồm bộ inlineđịnh nghĩa hàm mà không có extern, thì định nghĩa trong đơn vị dịch đó là một định nghĩa nội tuyến. Định nghĩa nội tuyến không cung cấp định nghĩa bên ngoài cho hàm và không cấm định nghĩa bên ngoài trong đơn vị dịch khác. Định nghĩa nội tuyến cung cấp một giải pháp thay thế cho định nghĩa bên ngoài mà người dịch có thể sử dụng để triển khai bất kỳ lệnh gọi hàm nào trong cùng một đơn vị dịch. Không xác định được liệu một lệnh gọi hàm sử dụng định nghĩa nội tuyến hay định nghĩa bên ngoài.

Nếu tôi hiểu tất cả điều này một cách chính xác, thì một đơn vị biên dịch có một hàm được định nghĩa inlinenhư trong ví dụ trên chỉ biên dịch nhất quán nếu cũng có một hàm bên ngoài có cùng tên và tôi không bao giờ biết liệu hàm của riêng mình hay hàm bên ngoài được gọi.

Đây không phải là hành vi hoàn toàn sáng suốt sao? Có bao giờ hữu ích khi xác định một hàm inlinekhông có statichoặc externtrong C99 không? Tui bỏ lỡ điều gì vậy?

Tóm tắt câu trả lời

Tất nhiên tôi đã thiếu một cái gì đó, và hành vi không phải là tốt. :)

Như Nemo giải thích , ý tưởng là đưa ra định nghĩa của hàm

inline void f() {}

trong tệp tiêu đề và chỉ một khai báo

extern inline void f();

trong tệp .c tương ứng. Chỉ có externkhai báo mới kích hoạt tạo mã nhị phân có thể nhìn thấy bên ngoài. Và thực sự là không có việc sử dụng inlinetrong tệp .c - nó chỉ hữu ích trong tiêu đề.

Như cơ sở lý luận của ủy ban C99 được trích dẫn trong câu trả lời của Jonathan đã giải thích, inlinelà tất cả về tối ưu hóa trình biên dịch yêu cầu định nghĩa của một hàm để hiển thị tại trang web của cuộc gọi. Điều này chỉ có thể đạt được bằng cách đặt định nghĩa trong tiêu đề và tất nhiên một định nghĩa trong tiêu đề không được phát ra mã mỗi khi trình biên dịch nhìn thấy nó. Nhưng vì trình biên dịch không bị buộc phải thực sự nội dòng một hàm, nên một định nghĩa bên ngoài phải tồn tại ở đâu đó.


bản sao đường viền của stackoverflow.com/questions/5369888/…
Earlz


@Earlz: Cảm ơn vì liên kết. Câu hỏi của tôi là về cơ sở lý do đằng sau đoạn trích dẫn của tiêu chuẩn, và nếu có bao giờ sử dụng cho inlinekhông staticextern, mặc dù. Thật không may, không có vấn đề nào được đề cập trong câu hỏi đó.
Sven Marnach

@Nemo: Tôi đã đọc câu hỏi đó và câu trả lời trước khi tôi đăng bài của mình. Một lần nữa, tôi không hỏi "nó hoạt động như thế nào?" mà đúng hơn là "ý tưởng đằng sau hành vi này là gì"? Tôi khá chắc chắn rằng tôi đang thiếu một cái gì đó ở đây.
Sven Marnach

1
vâng, đó là lý do tại sao tôi nói "đường biên giới". Cá nhân tôi nghĩ rằng đây là hỏi một câu hỏi hoàn toàn khác nhau
Earlz

Câu trả lời:


40

Trên thực tế, câu trả lời xuất sắc này cũng trả lời câu hỏi của bạn, tôi nghĩ:

Nội tuyến extern làm gì?

Ý tưởng là "nội tuyến" có thể được sử dụng trong tệp tiêu đề, và sau đó "nội tuyến ngoại" trong tệp .c. "extern inline" chỉ là cách bạn hướng dẫn trình biên dịch tệp đối tượng nào chứa mã được tạo (hiển thị bên ngoài).

[cập nhật, để giải thích]

Tôi không nghĩ rằng có bất kỳ cách sử dụng nào cho "inline" (không có "static" hoặc "extern") trong tệp .c. Nhưng trong tệp tiêu đề, nó có ý nghĩa và nó yêu cầu khai báo "nội tuyến bên ngoài" tương ứng trong một số tệp .c để thực sự tạo ra mã độc lập.


1
Cảm ơn, tôi đang bắt đầu có ý tưởng!
Sven Marnach

8
Vâng, bản thân tôi chưa bao giờ hiểu điều này cho đến tận bây giờ, vì vậy cảm ơn bạn đã hỏi :-). Điều kỳ lạ là định nghĩa "nội tuyến" không phải ngoại lai lại xuất hiện trong tiêu đề (nhưng không nhất thiết dẫn đến bất kỳ quá trình tạo mã nào), trong khi khai báo "nội tuyến bên ngoài" đi trong tệp .c và thực sự khiến mã được tạo ra.
Nemo

3
Điều này có nghĩa là tôi viết inline void f() {}trong tiêu đề và extern inline void f();trong tệp .c? Vì vậy, định nghĩa hàm thực sự đi trong tiêu đề và tệp .c chỉ chứa một khai báo trong trường hợp này, đảo ngược thứ tự thông thường?
Sven Marnach

1
@endolith: Tôi không hiểu câu hỏi của bạn. Chúng tôi đang thảo luận về việc inline không có static hoặc extern. Tất nhiên static inlinelà tốt, nhưng đó không phải là những gì câu hỏi và câu trả lời này là về.
Nemo

2
@MatthieuMoy Bạn cần sử dụng -std=c99thay vì -std=gnu89.
a3f

27

Từ tiêu chuẩn (ISO / IEC 9899: 1999) chính nó:

Phụ lục J.2 Hành vi không xác định

  • ...
  • Một hàm có liên kết ngoài được khai báo với một bộ inlineđịnh nghĩa hàm, nhưng cũng không được định nghĩa trong cùng một đơn vị dịch (6.7.4).
  • ...

Ủy ban C99 đã viết Cơ sở lý luận , và nó nói:

6.7.4 Các thông số chức năng

Một tính năng mới của C99: Các inlinetừ khóa, chuyển thể từ C ++, là một chức năng-specifier có thể chỉ được sử dụng trong khai báo hàm. Nó hữu ích cho việc tối ưu hóa chương trình yêu cầu định nghĩa của một hàm để hiển thị tại trang web của cuộc gọi. (Lưu ý rằng Tiêu chuẩn không cố gắng chỉ rõ bản chất của những tối ưu hóa này.)

Khả năng hiển thị được đảm bảo nếu hàm có liên kết nội bộ hoặc nếu nó có liên kết bên ngoài và lệnh gọi nằm trong cùng một đơn vị dịch với định nghĩa bên ngoài. Trong những trường hợp này, sự hiện diện của inlinetừ khóa trong khai báo hoặc định nghĩa của hàm không có tác dụng gì ngoài việc chỉ ra tùy chọn rằng các lệnh gọi của hàm đó nên được tối ưu hóa theo sở thích so với các lệnh gọi của các hàm khác được khai báo mà không có inlinetừ khóa.

Khả năng hiển thị là một vấn đề đối với lệnh gọi của một hàm có liên kết ngoài trong đó lệnh gọi nằm trong một đơn vị dịch khác với định nghĩa của hàm. Trong trường hợp này, inlinetừ khóa cho phép đơn vị dịch có chứa lệnh gọi cũng chứa định nghĩa cục bộ hoặc nội dòng của hàm.

Một chương trình có thể chứa đơn vị dịch với định nghĩa bên ngoài, đơn vị dịch có định nghĩa nội tuyến và đơn vị dịch có khai báo nhưng không có định nghĩa cho một hàm. Các cuộc gọi trong đơn vị dịch sau sẽ sử dụng định nghĩa bên ngoài như bình thường.

Định nghĩa nội tuyến của một hàm được coi là một định nghĩa khác với định nghĩa ngoại vi. Nếu một cuộc gọi đến một số chức năng funccó liên kết bên ngoài xảy ra khi định nghĩa nội tuyến được hiển thị, thì hành vi giống như khi cuộc gọi được thực hiện cho một chức năng khác __funcvới liên kết nội bộ. Một chương trình phù hợp không được phụ thuộc vào hàm nào được gọi. Đây là mô hình nội tuyến trong Tiêu chuẩn.

Một chương trình tuân thủ không được dựa vào việc triển khai bằng định nghĩa nội tuyến, cũng như không được dựa vào việc triển khai bằng định nghĩa bên ngoài. Địa chỉ của một hàm luôn là địa chỉ tương ứng với định nghĩa bên ngoài, nhưng khi địa chỉ này được sử dụng để gọi hàm, định nghĩa nội tuyến có thể được sử dụng. Do đó, ví dụ sau có thể không hoạt động như mong đợi.

inline const char *saddr(void)
{
    static const char name[] = "saddr";
    return name;
}
int compare_name(void)
{
    return saddr() == saddr(); // unspecified behavior
}

Vì việc triển khai có thể sử dụng định nghĩa nội tuyến cho một trong các lệnh gọi đến saddrvà sử dụng định nghĩa bên ngoài cho lệnh gọi kia, nên hoạt động bình đẳng không được đảm bảo đánh giá thành 1 (đúng). Điều này cho thấy rằng các đối tượng tĩnh được xác định trong định nghĩa nội tuyến khác với đối tượng tương ứng của chúng trong định nghĩa bên ngoài. Điều này thúc đẩy ràng buộc chống lại việc xác định một không phải constđối tượng của loại này.

Nội tuyến đã được thêm vào Tiêu chuẩn theo cách mà nó có thể được triển khai bằng công nghệ trình liên kết hiện có và một tập con nội tuyến C99 tương thích với C ++. Điều này đạt được bằng cách yêu cầu chính xác một đơn vị dịch chứa định nghĩa của một hàm nội tuyến được chỉ định làm đơn vị cung cấp định nghĩa bên ngoài cho hàm. Bởi vì đặc tả đó chỉ bao gồm một khai báo thiếu inlinetừ khóa hoặc chứa cả hai inlineextern, nó cũng sẽ được chấp nhận bởi trình dịch C ++.

Nội tuyến trong C99 mở rộng đặc tả C ++ theo hai cách. Đầu tiên, nếu một hàm được khai báo inlinetrong một đơn vị dịch, thì nó không cần phải được khai báo inlinetrong mọi đơn vị dịch khác. Điều này cho phép, ví dụ, một hàm thư viện được nội tuyến trong thư viện nhưng chỉ có sẵn thông qua một định nghĩa bên ngoài ở nơi khác. Việc thay thế sử dụng một hàm bao bọc cho hàm bên ngoài yêu cầu một tên bổ sung; và nó cũng có thể tác động xấu đến hiệu suất nếu một người dịch không thực sự thay thế nội tuyến.

Thứ hai, yêu cầu rằng tất cả các định nghĩa của một hàm nội tuyến phải "hoàn toàn giống nhau" được thay thế bằng yêu cầu rằng hành vi của chương trình không được phụ thuộc vào việc một lệnh gọi được thực hiện với định nghĩa nội tuyến hiển thị hay định nghĩa bên ngoài, của một chức năng. Điều này cho phép một định nghĩa nội tuyến được chuyên biệt để sử dụng trong một đơn vị dịch cụ thể. Ví dụ: định nghĩa bên ngoài của một hàm thư viện có thể bao gồm một số xác thực đối số không cần thiết cho các lệnh gọi được thực hiện từ các hàm khác trong cùng một thư viện. Những phần mở rộng này cung cấp một số lợi thế; và các lập trình viên lo ngại về tính tương thích có thể chỉ cần tuân theo các quy tắc C ++ chặt chẽ hơn.

Lưu ý rằng việc triển khai cung cấp các định nghĩa nội tuyến về các hàm thư viện chuẩn trong tiêu đề chuẩn là không thích hợp vì điều này có thể phá vỡ một số mã kế thừa khai báo lại các hàm thư viện chuẩn sau khi bao gồm các tiêu đề của chúng. Các inlinetừ khóa chỉ nhằm mục đích cung cấp cho người dùng với một cách cầm tay cho thấy nội tuyến chức năng. Bởi vì các tiêu đề tiêu chuẩn không cần phải di động, việc triển khai có các tùy chọn khác dọc theo các dòng:

#define abs(x) __builtin_abs(x)

hoặc các cơ chế không di động khác để nội tuyến các chức năng thư viện tiêu chuẩn.


Cảm ơn, điều này rất chi tiết - và có thể đọc được như bản thân tiêu chuẩn. :-) Tôi biết tôi phải thiếu một cái gì đó. Bạn có thể cho một tài liệu tham khảo mà bạn lấy cái này từ đâu?
Sven Marnach

Nó chỉ vượt qua tâm trí của tôi mà bạn có thể sử dụng Google để tìm các liên kết: open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf
Sven Marnach

@Sven: Tôi đã mượn URL từ tìm kiếm của bạn và đưa nó vào câu trả lời. Tài liệu tôi sử dụng là bản sao của cơ sở lý luận mà tôi đã lưu trước đây (năm 2005), nhưng nó cũng là V5.10.
Jonathan Leffler

4
Cảm ơn một lần nữa. Cái nhìn sâu sắc có giá trị nhất mà tôi nhận được từ câu trả lời là có một tài liệu chứa cơ sở lý luận của tiêu chuẩn C99 (và có lẽ cũng có tài liệu cho các tiêu chuẩn khác). Trong khi câu trả lời của Nemo cho tôi một con cá thì câu này lại dạy tôi câu cá.
Sven Marnach

0

> Tôi gặp lỗi trình liên kết (tham chiếu không xác định đến f)

Hoạt động ở đây: Linux x86-64, GCC 4.1.2. Có thể là một lỗi trong trình biên dịch của bạn; Tôi không thấy bất cứ điều gì trong đoạn trích dẫn từ tiêu chuẩn cấm chương trình đã cho. Lưu ý việc sử dụng if thay vì iff .

Định nghĩa nội tuyến cung cấp một giải pháp thay thế cho định nghĩa bên ngoài mà người dịch có thể sử dụng để triển khai bất kỳ lệnh gọi hàm nào trong cùng một đơn vị dịch.

Vì vậy, nếu bạn biết hoạt động của hàm fvà bạn muốn gọi nó trong một vòng lặp chặt chẽ, bạn có thể sao chép-dán định nghĩa của nó vào một mô-đun để ngăn các cuộc gọi hàm; hoặc , bạn có thể cung cấp một định nghĩa tương đương với mục đích của mô-đun hiện tại (nhưng bỏ qua xác thực đầu vào hoặc bất kỳ tối ưu hóa nào bạn có thể tưởng tượng). Tuy nhiên, người viết trình biên dịch có tùy chọn tối ưu hóa kích thước chương trình.


2
Tiêu chuẩn nói rằng trình biên dịch có thể luôn giả định rằng có một hàm bên ngoài có cùng tên và gọi nó thay vì phiên bản nội tuyến, vì vậy tôi không nghĩ đó là lỗi của trình biên dịch. Tôi đã thử với gcc 4.3, 4.4 và 4.5, tất cả đều cho lỗi trình liên kết.
Sven Marnach
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.