Các biến nội tuyến hoạt động như thế nào?


124

Tại cuộc họp Tiêu chuẩn ISO C ++ của Oulu năm 2016, một đề xuất có tên là Biến nội tuyến đã được ủy ban tiêu chuẩn bỏ phiếu thành C ++ 17.

Theo thuật ngữ của giáo dân, các biến nội tuyến là gì, chúng hoạt động như thế nào và chúng hữu ích cho việc gì? Các biến nội tuyến nên được khai báo, định nghĩa và sử dụng như thế nào?


@jotik Tôi đoán hoạt động tương đương sẽ thay thế bất kỳ sự xuất hiện nào của biến bằng giá trị của nó. Thông thường điều này chỉ hợp lệ nếu biến là const.
melpomene

5
Đó không phải là điều duy nhất mà inlinetừ khóa làm cho các hàm. Các inlinetừ khóa, khi áp dụng cho các chức năng, có một tác dụng quan trọng khác, có thể dịch trực tiếp cho các biến. Một inlinehàm, có lẽ được khai báo trong tệp tiêu đề, sẽ không dẫn đến lỗi "ký hiệu trùng lặp" tại thời điểm liên kết, ngay cả khi tiêu đề được #included bởi nhiều đơn vị dịch. Các inlinetừ khóa, khi áp dụng cho các biến, sẽ có kết quả chính xác như nhau. Kết thúc.
Sam Varshavchik

4
^ Theo nghĩa 'thay thế bất kỳ lệnh gọi nào đến hàm này bằng một bản sao mã tại chỗ', inlinechỉ là một yêu cầu yếu, không ràng buộc đối với trình tối ưu hóa. Các trình biên dịch có thể tự do không nội dòng các hàm được yêu cầu và / hoặc nội dòng các hàm mà bạn không chú thích. Thay vào đó, mục đích thực tế của inlinetừ khóa là để loại bỏ nhiều lỗi định nghĩa.
underscore_d

Câu trả lời:


121

Câu đầu tiên của đề xuất:

Các inlinespecifier có thể được áp dụng cho các biến cũng như chức năng.

Tác dụng ¹đảm bảo inlinekhi được áp dụng cho một hàm, là cho phép hàm được xác định giống hệt nhau, với liên kết bên ngoài, trong nhiều đơn vị dịch. Đối với thực tế, có nghĩa là xác định chức năng trong tiêu đề, có thể được bao gồm trong nhiều đơn vị dịch. Đề xuất mở rộng khả năng này cho các biến.

Vì vậy, trong điều kiện thực tế, đề xuất (hiện đã được chấp nhận) cho phép bạn sử dụng inlinetừ khóa để xác định constbiến phạm vi không gian tên liên kết bên ngoài hoặc bất kỳ staticthành viên dữ liệu lớp nào , trong tệp tiêu đề, để nhiều định nghĩa dẫn đến khi tiêu đề đó được bao gồm trong nhiều đơn vị dịch là OK với trình liên kết - nó chỉ chọn một trong số chúng.

Cho đến khi và bao gồm C ++ 14, máy móc nội bộ cho điều này đã có ở đó, để hỗ trợ staticcác biến trong các mẫu lớp, nhưng không có cách nào thuận tiện để sử dụng máy móc đó. Người ta phải dùng đến các thủ thuật như

template< class Dummy >
struct Kath_
{
    static std::string const hi;
};

template< class Dummy >
std::string const Kath_<Dummy>::hi = "Zzzzz...";

using Kath = Kath_<void>;    // Allows you to write `Kath::hi`.

Từ C ++ 17 trở đi, tôi tin rằng người ta có thể viết

struct Kath
{
    static std::string const hi;
};

inline std::string const Kath::hi = "Zzzzz...";    // Simpler!

… Trong một tệp tiêu đề.

Đề xuất bao gồm từ ngữ

Một thành viên dữ liệu tĩnh nội tuyến có thể được định nghĩa trong định nghĩa lớp và có thể chỉ định bộ khởi tạo dấu ngoặc nhọn hoặc dấu bằng. Nếu thành viên được khai báo với trình constexprxác định, nó có thể được khai báo lại trong phạm vi không gian tên mà không có trình khởi tạo (cách sử dụng này không được dùng nữa; xem‌ DX). Các khai báo của các thành viên dữ liệu tĩnh khác sẽ không chỉ định dấu ngoặc nhọn hoặc dấu bằng trong‌

… Điều này cho phép những điều ở trên được đơn giản hóa hơn nữa để chỉ

struct Kath
{
    static inline std::string const hi = "Zzzzz...";    // Simplest!
};

… Như được ghi nhận bởi TC trong một bình luận cho câu trả lời này.

Ngoài ra,  ​constexprhàm chỉ định  inline cho các thành viên dữ liệu tĩnh cũng như các hàm.


Lưu ý:
¹ Đối với một hàm inlinecũng có tác dụng gợi ý về việc tối ưu hóa, rằng trình biên dịch nên thích thay thế các lệnh gọi của hàm này bằng thay thế trực tiếp mã máy của hàm. Có thể bỏ qua gợi ý này.


2
Ngoài ra, giới hạn const chỉ áp dụng cho các biến phạm vi không gian tên. Những cái thuộc phạm vi lớp (như Kath::hi) không nhất thiết phải là const.
TC

4
Các báo cáo mới hơn chỉ ra rằng consthạn chế hoàn toàn bị loại bỏ.
TC

2
@Nick: Vì Richard Smith ("người biên tập dự án" của ủy ban C ++ hiện tại) là một trong hai tác giả và vì anh ấy là "chủ sở hữu mã của giao diện người dùng Clang C ++", nên Clang đoán. Và cấu trúc được biên dịch với clang 3.9.0 tại Godbolt . Nó cảnh báo rằng các biến nội tuyến là một phần mở rộng C ++ 1z. Tôi không tìm thấy cách nào để chia sẻ lựa chọn nguồn và trình biên dịch và các tùy chọn, vì vậy liên kết chỉ đến trang web nói chung, xin lỗi.
Chúc mừng và hth. - Alf

1
tại sao lại cần từ khóa nội tuyến bên trong khai báo class / struct? Tại sao không cho phép đơn giản static std::string const hi = "Zzzzz...";?
sasha.sochka

2
@EmilianCioca: Không, bạn sẽ phạm lỗi về trật tự khởi tạo tĩnh . Singleton về cơ bản là một thiết bị để tránh điều đó.
Chúc mừng và hth. - Alf

15

Các biến nội tuyến rất giống với các hàm nội tuyến. Nó báo hiệu cho trình liên kết rằng chỉ một phiên bản của biến sẽ tồn tại, ngay cả khi biến được nhìn thấy trong nhiều đơn vị biên dịch. Trình liên kết cần đảm bảo rằng không có thêm bản sao nào được tạo.

Các biến nội tuyến có thể được sử dụng để xác định hình cầu trong thư viện chỉ tiêu đề. Trước C ++ 17, họ phải sử dụng các giải pháp thay thế (hàm nội tuyến hoặc hack mẫu).

Ví dụ, một cách giải quyết là sử dụng singleton của Meyer với một hàm nội tuyến:

inline T& instance()
{
  static T global;
  return global;
}

Có một số hạn chế với cách tiếp cận này, chủ yếu là về hiệu suất. Có thể tránh được chi phí này bằng các giải pháp mẫu, nhưng rất dễ làm sai.

Với các biến nội tuyến, bạn có thể trực tiếp khai báo nó (mà không gặp lỗi trình liên kết nhiều định nghĩa):

inline T global;

Ngoài thư viện chỉ tiêu đề, có những trường hợp khác mà các biến nội tuyến có thể giúp ích. Nir Friedman đề cập đến chủ đề này trong bài nói chuyện của anh ấy tại CppCon: Những gì các nhà phát triển C ++ nên biết về các hình cầu (và trình liên kết) . Phần về các biến nội tuyến và cách giải quyết bắt đầu ở 18 phút 9 giây .

Tóm lại, nếu bạn cần khai báo các biến toàn cục được chia sẻ giữa các đơn vị biên dịch, việc khai báo chúng dưới dạng các biến nội tuyến trong tệp tiêu đề là đơn giản và tránh được các vấn đề với các cách giải quyết trước C ++ 17.

(Ví dụ: vẫn có các trường hợp sử dụng cho singleton của Meyer, nếu bạn muốn khởi tạo lười biếng một cách rõ ràng.)


11

Ví dụ tối thiểu có thể chạy được

Tính năng C ++ 17 tuyệt vời này cho phép chúng tôi:

  • thuận tiện chỉ sử dụng một địa chỉ bộ nhớ duy nhất cho mỗi hằng số
  • lưu trữ nó dưới dạng constexpr: Làm thế nào để khai báo constexpr extern?
  • làm điều đó trong một dòng duy nhất từ ​​một tiêu đề

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

inline constexpr int notmain_i = 42;

const int* notmain_func();

#endif

notmain.cpp

#include "notmain.hpp"

const int* notmain_func() {
    return &notmain_i;
}

Biên dịch và chạy:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub ngược dòng .

Xem thêm: Biến nội tuyến hoạt động như thế nào?

Tiêu chuẩn C ++ về các biến nội tuyến

Tiêu chuẩn C ++ đảm bảo rằng các địa chỉ sẽ giống nhau. Bản nháp tiêu chuẩn C ++ 17 N4659 10.1.6 "Bộ chỉ định nội tuyến":

6 Một hàm hoặc biến nội tuyến có liên kết bên ngoài sẽ có cùng địa chỉ trong tất cả các đơn vị dịch.

cppreference https://en.cppreference.com/w/cpp/language/inline giải thích rằng nếu statickhông được cung cấp, thì nó có liên kết bên ngoài.

Triển khai biến nội tuyến GCC

Chúng ta có thể quan sát cách nó được triển khai với:

nm main.o notmain.o

trong đó có:

main.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
                 U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i

notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

man nmnói về u:

"u" Biểu tượng là một biểu tượng toàn cầu duy nhất. Đây là phần mở rộng GNU cho bộ tiêu chuẩn của các ràng buộc ký hiệu ELF. Đối với một ký hiệu như vậy, trình liên kết động sẽ đảm bảo rằng trong toàn bộ quá trình chỉ có một ký hiệu có tên và kiểu này được sử dụng.

vì vậy chúng tôi thấy rằng có một phần mở rộng ELF dành riêng cho việc này.

Pre-C ++ 17: extern const

Trước C ++ 17 và trong C, chúng ta có thể đạt được hiệu ứng rất giống với dấu extern const, dẫn đến việc sử dụng một vị trí bộ nhớ duy nhất.

Nhược điểm inlinelà:

  • không thể tạo biến constexprbằng kỹ thuật này, chỉ inlinecho phép rằng: Làm thế nào để khai báo constexpr extern?
  • nó kém thanh lịch hơn vì bạn phải khai báo và xác định biến riêng biệt trong tiêu đề và tệp cpp

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.cpp

#include "notmain.hpp"

const int notmain_i = 42;

const int* notmain_func() {
    return &notmain_i;
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

extern const int notmain_i;

const int* notmain_func();

#endif

GitHub ngược dòng .

Các lựa chọn thay thế chỉ dành cho tiêu đề Pre-C ++ 17

Đây không phải là externgiải pháp tốt, nhưng chúng hoạt động và chỉ chiếm một vị trí bộ nhớ duy nhất:

Một constexprhàm, bởi vì constexprhàm ýinlineinline cho phép (buộc) định nghĩa xuất hiện trên mọi đơn vị dịch :

constexpr int shared_inline_constexpr() { return 42; }

và tôi cá rằng bất kỳ trình biên dịch tốt nào cũng sẽ nội tuyến cuộc gọi.

Bạn cũng có thể sử dụng một consthoặc constexprbiến số nguyên tĩnh như trong:

#include <iostream>

struct MyClass {
    static constexpr int i = 42;
};

int main() {
    std::cout << MyClass::i << std::endl;
    // undefined reference to `MyClass::i'
    //std::cout << &MyClass::i << std::endl;
}

nhưng bạn không thể làm những việc như lấy địa chỉ của nó, hoặc nếu không nó sẽ được sử dụng bởi odr, hãy xem thêm: https://en.cppreference.com/w/cpp/language/static "Các thành viên tĩnh không đổi" và Định nghĩa dữ liệu tĩnh constexpr các thành viên

C

Trong C, tình huống cũng giống như C ++ trước C ++ 17, tôi đã tải lên một ví dụ tại: "static" có nghĩa là gì trong C?

Sự khác biệt duy nhất là trong C ++, constngụ ý staticcho toàn cầu, nhưng nó không có trong ngữ nghĩa C: C ++ của `static const` so với` const`

Bất kỳ cách nào để hoàn toàn nội tuyến nó?

TODO: có cách nào để nội dòng hoàn toàn biến mà không cần sử dụng bất kỳ bộ nhớ nào không?

Giống như những gì bộ tiền xử lý làm.

Điều này sẽ yêu cầu bằng cách nào đó:

  • cấm hoặc phát hiện nếu địa chỉ của biến được sử dụng
  • thêm thông tin đó vào các tệp đối tượng ELF và để LTO tối ưu hóa nó

Có liên quan:

Đã thử nghiệm trong Ubuntu 18.10, GCC 8.2.0.


2
inlinehầu như không liên quan gì đến nội tuyến, không liên quan đến hàm hay biến, mặc dù chính nó là từ. inlinekhông yêu cầu trình biên dịch nội tuyến bất cứ điều gì. Nó yêu cầu trình liên kết đảm bảo rằng chỉ có một định nghĩa, theo truyền thống, công việc của lập trình viên. Vì vậy, "Bất kỳ cách nào để hoàn toàn nội tuyến nó?" ít nhất là một câu hỏi hoàn toàn không liên quan.
không-một-người sử dụng
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.