Tại sao các mẫu chỉ có thể được thực hiện trong tệp tiêu đề?


1778

Trích dẫn từ thư viện chuẩn C ++: hướng dẫn và cẩm nang :

Cách duy nhất để sử dụng các mẫu tại thời điểm này là triển khai chúng trong các tệp tiêu đề bằng cách sử dụng các hàm nội tuyến.

Tại sao lại thế này?

(Làm rõ: tệp tiêu đề không phải là giải pháp di động duy nhất . Nhưng chúng là giải pháp di động tiện lợi nhất.)


13
Mặc dù đúng là việc đặt tất cả các định nghĩa hàm mẫu vào tệp tiêu đề có lẽ là cách thuận tiện nhất để sử dụng chúng, nhưng vẫn chưa rõ "nội tuyến" đang làm gì trong trích dẫn đó. Không cần sử dụng các hàm nội tuyến cho điều đó. "Nội tuyến" hoàn toàn không có gì để làm với điều này.
AnT

7
Sách đã hết hạn.
gerardw

1
Một mẫu không giống như một hàm có thể được biên dịch thành mã byte. Nó chỉ là một mô hình để tạo ra một chức năng như vậy. Nếu bạn tự đặt một mẫu vào tệp * .cpp, không có gì để biên dịch. Hơn nữa, instancit explancite thực sự không phải là một mẫu, mà là điểm khởi đầu để tạo một hàm ra khỏi mẫu mà kết thúc trong tệp * .obj.
dgrat

5
Tôi có phải là người duy nhất cảm thấy khái niệm mẫu bị tê liệt trong C ++ do điều này không? ...
DragonGamer

Câu trả lời:


1558

Nên biết trước: Đó là không cần thiết để đưa thi trong file header, xem giải pháp thay thế ở phần cuối của câu trả lời này.

Dù sao, lý do mã của bạn không thành công là vì khi khởi tạo một mẫu, trình biên dịch sẽ tạo một lớp mới với đối số mẫu đã cho. Ví dụ:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

Khi đọc dòng này, trình biên dịch sẽ tạo một lớp mới (hãy gọi nó FooInt), tương đương với sau:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

Do đó, trình biên dịch cần có quyền truy cập vào việc thực hiện các phương thức, để khởi tạo chúng với đối số khuôn mẫu (trong trường hợp này int). Nếu các triển khai này không có trong tiêu đề, chúng sẽ không thể truy cập được và do đó trình biên dịch sẽ không thể khởi tạo mẫu.

Một giải pháp phổ biến cho vấn đề này là viết khai báo mẫu trong tệp tiêu đề, sau đó triển khai lớp trong tệp thực hiện (ví dụ .tpp) và bao gồm tệp thực hiện này ở cuối tiêu đề.

Foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

Bằng cách này, việc thực hiện vẫn được tách ra khỏi khai báo, nhưng trình biên dịch có thể truy cập được.

Giải pháp thay thế

Một giải pháp khác là tách biệt việc thực hiện và khởi tạo rõ ràng tất cả các trường hợp mẫu bạn sẽ cần:

Foo.h

// no implementation
template <typename T> struct Foo { ... };

Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

Nếu lời giải thích của tôi không đủ rõ ràng, bạn có thể xem C-Super-FAQ về chủ đề này .


96
Trên thực tế, việc khởi tạo rõ ràng cần phải nằm trong tệp .cpp có quyền truy cập vào các định nghĩa cho tất cả các hàm thành viên của Foo, thay vì trong tiêu đề.
Nhân

11
"trình biên dịch cần có quyền truy cập vào việc thực hiện các phương thức, để khởi tạo chúng bằng đối số khuôn mẫu (trong trường hợp này là int). Nếu các triển khai này không có trong tiêu đề, chúng sẽ không thể truy cập được" Nhưng tại sao lại là một triển khai trong tập tin .cpp không truy cập được vào trình biên dịch? Một trình biên dịch cũng có thể truy cập thông tin .cpp, làm thế nào khác nó sẽ biến chúng thành các tệp .obj? EDIT: câu trả lời cho câu hỏi này nằm trong liên kết được cung cấp trong câu trả lời này ...
xcrypt

31
Tôi không nghĩ rằng điều này giải thích câu hỏi rõ ràng, điều quan trọng rõ ràng là liên quan đến ĐƠN VỊ tổng hợp không được đề cập trong bài đăng này
zinking

6
@Gabson: cấu trúc và các lớp tương đương với ngoại lệ rằng công cụ sửa đổi truy cập mặc định cho các lớp là "riêng tư", trong khi nó là công khai cho các cấu trúc. Có một số khác biệt nhỏ khác mà bạn có thể tìm hiểu bằng cách xem xét câu hỏi này .
Luc Touraille

3
Tôi đã thêm một câu vào lúc bắt đầu câu trả lời này để làm rõ rằng câu hỏi dựa trên tiền đề sai. Nếu ai đó hỏi "Tại sao X đúng?" trong khi thực tế X không đúng, chúng ta nên nhanh chóng từ chối giả định đó.
Aaron McDaid

250

Rất nhiều câu trả lời đúng ở đây, nhưng tôi muốn thêm câu này (cho đầy đủ):

Nếu bạn, ở dưới cùng của tệp cpp triển khai, thực hiện khởi tạo rõ ràng tất cả các loại mà mẫu sẽ được sử dụng, trình liên kết sẽ có thể tìm thấy chúng như bình thường.

Chỉnh sửa: Thêm ví dụ về khởi tạo mẫu rõ ràng. Được sử dụng sau khi mẫu đã được xác định và tất cả các chức năng thành viên đã được xác định.

template class vector<int>;

Điều này sẽ khởi tạo (và do đó cung cấp cho trình liên kết) lớp và tất cả các hàm thành viên của nó (chỉ). Cú pháp tương tự hoạt động cho các hàm mẫu, vì vậy nếu bạn có quá tải toán tử không phải là thành viên, bạn có thể cần phải làm tương tự cho các hàm đó.

Ví dụ trên khá vô dụng vì vectơ được xác định đầy đủ trong các tiêu đề, ngoại trừ khi một tệp bao gồm chung (tiêu đề được biên dịch trước?) Sử dụng extern template class vector<int>để giữ cho nó không khởi tạo nó trong tất cả các tệp (1000?) Khác sử dụng vectơ.


51
Ừ Câu trả lời tốt, nhưng không có giải pháp thực sự sạch. Liệt kê tất cả các loại có thể cho một mẫu dường như không phù hợp với những gì mẫu được cho là.
Jiminion

6
Điều này có thể tốt trong nhiều trường hợp nhưng thường phá vỡ mục đích của mẫu có nghĩa là cho phép bạn sử dụng lớp với bất kỳ typemà không liệt kê chúng theo cách thủ công.
Tomáš Zato - Phục hồi Monica

7
vectorkhông phải là một ví dụ tốt bởi vì một container vốn đã nhắm mục tiêu các loại "tất cả". Nhưng điều này xảy ra rất thường xuyên khi bạn tạo các mẫu chỉ dành cho một nhóm loại cụ thể, ví dụ như các kiểu số: int8_t, int16_t, int32_t, uint8_t, uint16_t, v.v. Trong trường hợp này, vẫn hợp lý khi sử dụng một mẫu , nhưng rõ ràng việc khởi tạo chúng cho toàn bộ các loại cũng có thể và theo ý kiến ​​của tôi, được khuyến nghị.
ChúZeiv

Được sử dụng sau khi mẫu đã được xác định, "và tất cả các chức năng thành viên đã được xác định". Cảm ơn !
Vitt Volt

1
Tôi cảm thấy như mình đang thiếu thứ gì đó. Tôi đặt phần khởi tạo rõ ràng cho hai loại vào .cpptệp của lớp và hai phần khởi tạo được tham chiếu từ các .cpptệp khác và tôi vẫn gặp lỗi liên kết mà các thành viên không tìm thấy.
oarfish

250

Đó là vì yêu cầu biên dịch riêng biệt và bởi vì các mẫu là đa hình kiểu khởi tạo.

Hãy đến gần hơn một chút để giải thích. Nói rằng tôi đã có các tệp sau:

  • foo.h
    • tuyên bố giao diện của class MyClass<T>
  • foo.cpp
    • định nghĩa việc thực hiện class MyClass<T>
  • bar.cpp
    • sử dụng MyClass<int>

Phương tiện biên soạn riêng tôi sẽ có thể biên dịch Foo.cpp một cách độc lập từ bar.cpp . Trình biên dịch thực hiện tất cả các công việc khó khăn để phân tích, tối ưu hóa và tạo mã trên mỗi đơn vị biên dịch hoàn toàn độc lập; chúng ta không cần phải phân tích toàn bộ chương trình. Chỉ có trình liên kết cần xử lý toàn bộ chương trình cùng một lúc và công việc của trình liên kết dễ dàng hơn nhiều.

bar.cpp thậm chí không cần phải tồn tại khi tôi biên dịch Foo.cpp , nhưng tôi vẫn sẽ có thể liên kết các foo.o tôi đã có cùng với bar.o tôi đã chỉ mới sản xuất, mà không cần phải biên dịch lại foo .cpp . foo.cpp thậm chí có thể được biên dịch thành một thư viện động, được phân phối ở một nơi khác mà không có foo.cpp và được liên kết với mã họ viết nhiều năm sau khi tôi viết foo.cpp .

"Đa hình theo kiểu tức thời" có nghĩa là mẫu MyClass<T>không thực sự là một lớp chung có thể được biên dịch thành mã có thể hoạt động với bất kỳ giá trị nào T. Điều đó sẽ thêm chi phí như boxing, cần phải vượt qua con trỏ hàm để allocators và nhà thầu vv Mục đích của C ++ mẫu là để tránh phải viết gần như giống hệt class MyClass_int, class MyClass_float, vv, nhưng vẫn có thể kết thúc với mã biên dịch đó là chủ yếu như thể chúng tôi đã viết riêng từng phiên bản. Vì vậy, một mẫu có nghĩa đen là một mẫu; một mẫu lớp không phải là một lớp, nó là một công thức để tạo một lớp mới cho mỗi Tchúng ta gặp phải. Một mẫu không thể được biên dịch thành mã, chỉ có thể biên dịch kết quả của mẫu.

Vì vậy, khi foo.cpp được biên dịch, trình biên dịch không thể thấy bar.cpp để biết điều đó MyClass<int>là cần thiết. Nó có thể nhìn thấy mẫu MyClass<T>, nhưng nó không thể phát ra mã cho điều đó (đó là một mẫu chứ không phải một lớp). Và khi bar.cpp được biên dịch, trình biên dịch có thể thấy rằng nó cần tạo một MyClass<int>, nhưng nó không thể nhìn thấy mẫu MyClass<T>(chỉ giao diện của nó trong foo.h ) vì vậy nó không thể tạo ra nó.

Nếu foo.cpp tự sử dụng MyClass<int>, thì mã đó sẽ được tạo trong khi biên dịch foo.cpp , vì vậy khi bar.o được liên kết với foo.o chúng có thể được nối và sẽ hoạt động. Chúng ta có thể sử dụng thực tế đó để cho phép một tập hợp hữu hạn các mẫu tức thời được triển khai trong tệp .cpp bằng cách viết một mẫu duy nhất. Nhưng không có cách nào để bar.cpp sử dụng mẫu làm mẫu và khởi tạo nó trên bất kỳ loại nào nó thích; nó chỉ có thể sử dụng các phiên bản có sẵn của lớp templated mà tác giả của foo.cpp nghĩ để cung cấp.

Bạn có thể nghĩ rằng khi biên dịch một mẫu, trình biên dịch sẽ "tạo ra tất cả các phiên bản", với các phiên bản không bao giờ được sử dụng sẽ được lọc ra trong quá trình liên kết. Ngoài chi phí rất lớn và những khó khăn cực kỳ mà cách tiếp cận sẽ gặp phải vì các tính năng "sửa đổi kiểu" như con trỏ và mảng cho phép ngay cả các loại tích hợp cũng tạo ra vô số loại, điều xảy ra khi tôi mở rộng chương trình của mình bằng cách thêm:

  • baz.cpp
    • tuyên bố và thực hiện class BazPrivate, và sử dụngMyClass<BazPrivate>

Không có cách nào có thể làm việc này trừ khi chúng ta

  1. Phải biên dịch lại foo.cpp mỗi khi chúng tôi thay đổi bất kỳ tệp nào khác trong chương trình , trong trường hợp nó thêm một phần khởi tạo tiểu thuyết mới củaMyClass<T>
  2. Yêu cầu baz.cpp chứa (có thể thông qua tiêu đề bao gồm) mẫu đầy đủ của MyClass<T>, để trình biên dịch có thể tạo MyClass<BazPrivate>trong quá trình biên dịch baz.cpp .

Không ai thích (1), bởi vì các hệ thống biên dịch phân tích toàn bộ chương trình mất mãi mãi để biên dịch và vì nó không thể phân phối các thư viện đã biên dịch mà không có mã nguồn. Vì vậy, chúng tôi có (2) thay thế.


50
nhấn mạnh trích dẫn một mẫu có nghĩa đen là một mẫu; một mẫu lớp không phải là một lớp, đó là một công thức để tạo một lớp mới cho mỗi T chúng ta bắt gặp
v.oddou

Tôi muốn biết, liệu có thể thực hiện các cảnh báo rõ ràng từ một nơi nào khác ngoài tệp tiêu đề hoặc tệp nguồn của lớp không? Ví dụ, làm chúng trong main.cpp?
gromit190

1
@Birger Bạn sẽ có thể thực hiện nó từ bất kỳ tệp nào có quyền truy cập vào triển khai mẫu đầy đủ (vì trong cùng một tệp hoặc thông qua tiêu đề bao gồm).
Ben

11
@ajeh Nó không khoa trương. Câu hỏi là "tại sao bạn phải triển khai các mẫu trong một tiêu đề?", Vì vậy tôi đã giải thích các lựa chọn kỹ thuật mà ngôn ngữ C ++ đưa ra dẫn đến yêu cầu này. Trước khi tôi viết câu trả lời của mình, những người khác đã cung cấp cách giải quyết không phải là giải pháp đầy đủ, vì không thể có giải pháp đầy đủ. Tôi cảm thấy những câu trả lời đó sẽ được bổ sung bằng một cuộc thảo luận đầy đủ hơn về góc độ "tại sao" của câu hỏi.
Ben

1
hãy tưởng tượng theo cách này mọi người ... nếu bạn không sử dụng các mẫu (để mã hóa hiệu quả những gì bạn cần), bạn sẽ chỉ cung cấp một vài phiên bản của lớp đó. Vì vậy, bạn có 3 lựa chọn. 1). không sử dụng các mẫu. (giống như tất cả các lớp / hàm khác, không ai quan tâm rằng những người khác không thể thay đổi các loại) 2). sử dụng các mẫu và tài liệu loại nào họ có thể sử dụng. 3). cung cấp cho họ toàn bộ phần thưởng (nguồn) 4). cung cấp cho họ toàn bộ nguồn trong trường hợp họ muốn tạo mẫu từ một lớp khác của bạn;)
Puddle

81

Các mẫu cần phải được trình biên dịch khởi tạo trước khi thực sự biên dịch chúng thành mã đối tượng. Việc khởi tạo này chỉ có thể đạt được nếu biết các đối số mẫu. Bây giờ hãy tưởng tượng một kịch bản trong đó một hàm mẫu được khai báo a.h, được định nghĩa a.cppvà sử dụng trong b.cpp. Khi a.cppđược biên dịch, không nhất thiết phải biết rằng quá trình biên dịch sắp tới b.cppsẽ yêu cầu một thể hiện của mẫu, chứ chưa nói đến trường hợp cụ thể nào. Đối với nhiều tệp tiêu đề và nguồn, tình huống có thể nhanh chóng trở nên phức tạp hơn.

Người ta có thể lập luận rằng các trình biên dịch có thể được làm cho thông minh hơn để "nhìn về phía trước" cho tất cả các sử dụng của mẫu, nhưng tôi chắc chắn rằng sẽ không khó để tạo ra các kịch bản đệ quy hoặc phức tạp khác. AFAIK, trình biên dịch không làm như vậy nhìn. Như Anton đã chỉ ra, một số trình biên dịch hỗ trợ khai báo xuất khẩu rõ ràng của các khởi tạo mẫu, nhưng không phải tất cả các trình biên dịch đều hỗ trợ nó (chưa?).


1
"xuất khẩu" là tiêu chuẩn, nhưng thật khó để thực hiện nên hầu hết các nhóm biên dịch chưa hoàn thành.
vava

5
xuất khẩu không loại bỏ nhu cầu tiết lộ nguồn, cũng không làm giảm phụ thuộc biên dịch, trong khi nó đòi hỏi một nỗ lực lớn từ các nhà xây dựng trình biên dịch. Vì vậy, Herb Sutter đã yêu cầu các nhà xây dựng trình biên dịch 'quên đi' xuất khẩu. Vì đầu tư thời gian cần thiết sẽ được chi tiêu tốt hơn ở nơi khác ...
Pieter

2
Vì vậy, tôi không nghĩ rằng xuất khẩu không được thực hiện "chưa". Nó có lẽ sẽ không bao giờ được thực hiện bởi bất kỳ ai khác ngoài EDG sau khi những người khác nhìn thấy nó mất bao lâu và thu được ít như thế nào
Pieter

3
Nếu điều đó làm bạn quan tâm, bài báo có tên "Tại sao chúng tôi không đủ khả năng xuất khẩu", nó được liệt kê trên blog của anh ấy ( gotw.ca/publications ) nhưng không có pdf ở đó (một google nhanh chóng nên bật lên)
Pieter

1
Ok, cảm ơn cho ví dụ tốt và giải thích. Đây là câu hỏi của tôi: tại sao trình biên dịch không thể tìm ra mẫu được gọi ở đâu và biên dịch các tệp đó trước khi biên dịch tệp định nghĩa? Tôi có thể tưởng tượng nó có thể được thực hiện trong một trường hợp đơn giản ... Có phải câu trả lời là sự phụ thuộc lẫn nhau sẽ làm xáo trộn trật tự khá nhanh?
Vlad

62

Trên thực tế, trước khi C ++ 11 tiêu chuẩn xác định exporttừ khóa đó sẽ làm cho nó có thể tuyên bố mẫu trong một tập tin header và thực hiện chúng ở nơi khác.

Không có trình biên dịch phổ biến nào thực hiện từ khóa này. Cái duy nhất tôi biết là frontend được viết bởi Edison Design Group, được sử dụng bởi trình biên dịch Comeau C ++. Tất cả những người khác yêu cầu bạn phải viết các mẫu trong các tệp tiêu đề, bởi vì trình biên dịch cần định nghĩa mẫu để khởi tạo đúng (như những người khác đã chỉ ra).

Do đó, ủy ban tiêu chuẩn ISO C ++ đã quyết định loại bỏ exporttính năng của các mẫu với C ++ 11.


6
... Và một vài năm sau đó, cuối cùng tôi đã hiểu những gì exportthực sự sẽ mang lại cho chúng tôi, và những gì không ... và bây giờ tôi hoàn toàn đồng ý với người EDG: Nó sẽ không mang lại cho chúng tôi hầu hết mọi người (bản thân tôi trong '11 bao gồm) nghĩ rằng nó sẽ, và tiêu chuẩn C ++ sẽ tốt hơn nếu không có nó.
DevSolar

4
@DevSolar: bài viết này là chính trị, lặp đi lặp lại và viết xấu. đó không phải là văn xuôi tiêu chuẩn thông thường ở đó. Không lâu dài và nhàm chán, nói về cơ bản 3 lần những điều tương tự trên hàng chục trang. Nhưng tôi được thông báo rằng xuất khẩu không phải là xuất khẩu. Đó là một thông tin tốt!
v.oddou

1
@ v.oddou: Nhà phát triển giỏi và nhà văn kỹ thuật giỏi là hai kỹ năng riêng biệt. Một số có thể làm cả hai, nhiều không thể. ;-)
DevSolar

@ v.oddou Bài báo không được viết quá tệ, đó là thông tin sai lệch. Ngoài ra, đó là một sự quay vòng trong thực tế: những lý lẽ thực sự cực kỳ mạnh mẽ đối với xuất khẩu được trộn lẫn theo cách làm cho nó có vẻ như chống lại xuất khẩu: Khám phá ra nhiều lỗ hổng ODRrelated trong tiêu chuẩn xuất hiện. Trước khi xuất, các vi phạm ODR không phải được trình biên dịch chẩn đoán. Bây giờ, điều đó là cần thiết bởi vì bạn cần kết hợp các cấu trúc dữ liệu nội bộ từ các đơn vị dịch thuật khác nhau và bạn không thể kết hợp chúng nếu chúng thực sự đại diện cho những thứ khác nhau, vì vậy bạn cần thực hiện kiểm tra.
tò mò

" bây giờ phải thêm đơn vị dịch thuật khi nó xảy ra " Duh. Khi bạn bị buộc phải sử dụng các đối số khập khiễng, bạn không có bất kỳ đối số nào. Tất nhiên bạn sẽ đề cập đến tên tập tin trong lỗi của bạn, những gì đối phó? Rằng bất cứ ai rơi vào BS đó là tâm trí bogg. " Ngay cả các chuyên gia như James Kanze cũng khó chấp nhận rằng xuất khẩu thực sự là như thế này. " CÁI GÌ? !!!!
tò mò

34

Mặc dù C ++ tiêu chuẩn không có yêu cầu như vậy, một số trình biên dịch yêu cầu tất cả các mẫu hàm và lớp cần phải được cung cấp trong mọi đơn vị dịch thuật mà chúng được sử dụng. Trong thực tế, đối với các trình biên dịch đó, phần thân của các hàm mẫu phải được cung cấp trong tệp tiêu đề. Lặp lại: điều đó có nghĩa là các trình biên dịch đó sẽ không cho phép chúng được xác định trong các tệp không phải tiêu đề, chẳng hạn như các tệp .cpp

Có một từ khóa xuất khẩu được cho là để giảm thiểu vấn đề này, nhưng nó gần như không thể mang theo được.


Tại sao tôi không thể triển khai chúng trong tệp .cpp với từ khóa "nội tuyến"?
MainID

2
Bạn có thể, và bạn không cần phải đặt "nội tuyến". Nhưng bạn có thể sử dụng chúng chỉ trong tệp cpp đó và không ở đâu khác.
vava

10
Đây gần như là câu trả lời chính xác nhất , ngoại trừ "điều đó có nghĩa là các trình biên dịch đó sẽ không cho phép chúng được xác định trong các tệp không phải tiêu đề như tệp .cpp" là hoàn toàn sai.
Các cuộc đua nhẹ nhàng trong quỹ đạo

28

Các mẫu phải được sử dụng trong các tiêu đề vì trình biên dịch cần khởi tạo các phiên bản mã khác nhau, tùy thuộc vào các tham số đã cho / suy ra cho các tham số mẫu. Hãy nhớ rằng một mẫu không đại diện trực tiếp cho mã, nhưng một mẫu cho một số phiên bản của mã đó. Khi bạn biên dịch một hàm không phải mẫu trong một .cpptệp, bạn đang biên dịch một hàm / lớp cụ thể. Đây không phải là trường hợp của các mẫu, có thể được khởi tạo bằng các loại khác nhau, cụ thể là mã bê tông phải được phát ra khi thay thế các tham số mẫu bằng các loại cụ thể.

Có một tính năng với exporttừ khóa được sử dụng để biên dịch riêng. Các exporttính năng bị phản đối trong C++11và, AFAIK, chỉ có một trình biên dịch thực hiện nó. Bạn không nên sử dụng export. Việc biên dịch riêng biệt là không thể trong C++hoặc C++11nhưng có thể trong C++17, nếu các khái niệm tạo ra nó, chúng ta có thể có một số cách biên dịch riêng biệt.

Để đạt được sự biên dịch riêng biệt, phải có thể kiểm tra thân mẫu riêng biệt. Có vẻ như một giải pháp là có thể với các khái niệm. Hãy xem bài báo này được trình bày gần đây tại cuộc họp tiêu chuẩn. Tôi nghĩ rằng đây không phải là yêu cầu duy nhất, vì bạn vẫn cần khởi tạo mã cho mã mẫu trong mã người dùng.

Vấn đề biên dịch riêng cho các mẫu Tôi đoán đó cũng là một vấn đề phát sinh khi di chuyển sang các mô-đun, hiện đang được xử lý.


15

Điều đó có nghĩa là cách dễ mang theo nhất để định nghĩa các cài đặt phương thức của các lớp mẫu là định nghĩa chúng bên trong định nghĩa lớp mẫu.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

15

Mặc dù có rất nhiều lời giải thích tốt ở trên, tôi vẫn thiếu một cách thực tế để tách các mẫu thành tiêu đề và phần thân.
Mối quan tâm chính của tôi là tránh biên dịch lại tất cả người dùng mẫu, khi tôi thay đổi định nghĩa của nó.
Có tất cả các khởi tạo mẫu trong thân mẫu không phải là một giải pháp khả thi đối với tôi, vì tác giả mẫu có thể không biết tất cả nếu việc sử dụng nó và người dùng mẫu có thể không có quyền sửa đổi nó.
Tôi đã sử dụng cách tiếp cận sau, cũng hoạt động cho các trình biên dịch cũ hơn (gcc 4.3.4, aCC A.03.13).

Đối với mỗi lần sử dụng mẫu, có một typedef trong tệp tiêu đề của riêng nó (được tạo từ mô hình UML). Phần thân của nó chứa phần khởi tạo (kết thúc trong một thư viện được liên kết ở cuối).
Mỗi người dùng của mẫu bao gồm tệp tiêu đề đó và sử dụng typedef.

Một ví dụ sơ đồ:

MyTemplate.h:

#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}

MyInstantiatedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstantiatedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;

chính.cpp:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}

Bằng cách này, chỉ các phần khởi tạo mẫu sẽ cần được biên dịch lại, không phải tất cả người dùng mẫu (và phụ thuộc).


1
Tôi thích cách tiếp cận này ngoại trừ các MyInstantiatedTemplate.htập tin và MyInstantiatedTemplateloại được thêm vào . Sẽ sạch hơn một chút nếu bạn không sử dụng nó, imho. Kiểm tra câu trả lời của tôi cho một câu hỏi khác hiển thị điều này: stackoverflow.com/a/41292751/4612476
Cameron Tacklind

Điều này có tốt nhất của hai thế giới. Tôi muốn câu trả lời này được đánh giá cao hơn! Cũng xem liên kết ở trên để thực hiện một chút sạch hơn của cùng một ý tưởng.
Wormer

8

Chỉ cần thêm một cái gì đó đáng chú ý ở đây. Người ta có thể định nghĩa các phương thức của một lớp templated chỉ tốt trong tệp thực hiện khi chúng không phải là các mẫu hàm.


myQueue.hpp:

template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    

myQueue.cpp:

// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}

2
Đối với người đàn ông thực sự ??? Nếu đó là sự thật thì câu trả lời của bạn phải được kiểm tra là chính xác. Tại sao mọi người cần tất cả những thứ voodo hacky đó nếu bạn chỉ có thể định nghĩa các phương thức thành viên không phải mẫu trong .cpp?
Michael IV

Chà điều đó không hoạt động. Ít nhất là trên MSVC 2019, nhận được biểu tượng bên ngoài chưa được giải quyết cho một chức năng thành viên của lớp mẫu.
Michael IV

Tôi không có MSVC 2019 để kiểm tra. Điều này được cho phép theo tiêu chuẩn C ++. Bây giờ, MSVC nổi tiếng vì không phải lúc nào cũng tuân thủ các quy tắc. Nếu bạn chưa có, hãy thử Cài đặt dự án -> C / C ++ -> Ngôn ngữ -> Chế độ phù hợp -> Có (cho phép-).
Nikos

1
Ví dụ chính xác này hoạt động nhưng sau đó bạn không thể gọi isEmptytừ bất kỳ đơn vị dịch thuật nào khác ngoài myQueue.cpp...
MM

7

Nếu mối quan tâm là thời gian biên dịch thêm và phình kích thước nhị phân được tạo ra bằng cách biên dịch .h như là một phần của tất cả các mô-đun .cpp sử dụng nó, trong nhiều trường hợp, điều bạn có thể làm là làm cho lớp mẫu giảm xuống từ lớp cơ sở không được tạo khuôn mẫu cho các phần không phụ thuộc kiểu của giao diện và lớp cơ sở đó có thể có phần triển khai của nó trong tệp .cpp.


2
Phản ứng này nên được sửa đổi khá nhiều. Tôi " độc lập " phát hiện ra cách tiếp cận tương tự của bạn và đặc biệt tìm kiếm người khác đã sử dụng nó, vì tôi tò mò liệu đây có phải là mẫu chính thức hay không và liệu nó có tên không. Cách tiếp cận của tôi là thực hiện class XBasebất cứ nơi nào tôi cần để thực hiện a template class X, đưa các bộ phận phụ thuộc vào loại Xvà tất cả các phần còn lại vào XBase.
Fabio A.

6

Điều đó là chính xác bởi vì trình biên dịch phải biết nó thuộc loại nào để phân bổ. Vì vậy, các lớp mẫu, hàm, enums, v.v. cũng phải được triển khai trong tệp tiêu đề nếu nó được đặt ở chế độ công khai hoặc một phần của thư viện (tĩnh hoặc động) vì các tệp tiêu đề KHÔNG được biên dịch không giống như các tệp c / cpp Chúng tôi. Nếu trình biên dịch không biết kiểu thì không thể biên dịch nó. Trong .Net có thể vì tất cả các đối tượng xuất phát từ lớp Object. Đây không phải là .Net.


5
"tập tin tiêu đề KHÔNG được biên dịch" - đó là một cách thực sự kỳ quặc để mô tả nó. Các tệp tiêu đề có thể là một phần của đơn vị dịch thuật, giống như tệp "c / cpp".
Flexo

2
Trên thực tế, nó gần như ngược lại với sự thật, đó là các tệp tiêu đề được biên dịch rất thường xuyên nhiều lần, trong khi một tệp nguồn thường được biên dịch một lần.
xaxxon

6

Trình biên dịch sẽ tạo mã cho mỗi khởi tạo mẫu khi bạn sử dụng một mẫu trong bước biên dịch. Trong quá trình biên dịch và liên kết, các tệp .cpp được chuyển đổi thành đối tượng thuần hoặc mã máy có chứa các tham chiếu hoặc ký hiệu không xác định vì các tệp .h được bao gồm trong tệp main.cpp của bạn không có YET thực hiện. Chúng đã sẵn sàng để được liên kết với một tệp đối tượng khác xác định việc triển khai cho mẫu của bạn và do đó bạn có một tệp thực thi a.out đầy đủ.

Tuy nhiên, vì các mẫu cần được xử lý trong bước biên dịch để tạo mã cho từng khởi tạo mẫu mà bạn xác định, do đó, chỉ cần biên dịch một mẫu riêng biệt khỏi tệp tiêu đề của nó sẽ không hoạt động vì chúng luôn luôn đi đôi với nhau. rằng mỗi khởi tạo mẫu là một lớp hoàn toàn mới theo nghĩa đen. Trong một lớp thông thường, bạn có thể tách .h và .cpp vì .h là một bản thiết kế của lớp đó và .cpp là phần triển khai thô nên mọi tệp thực thi có thể được biên dịch và liên kết thường xuyên, tuy nhiên sử dụng các mẫu .h là bản kế hoạch lớp sẽ trông không phải là đối tượng trông như thế nào có nghĩa là một tệp .cpp mẫu không phải là một triển khai thường xuyên thô của một lớp, nó chỉ đơn giản là một bản thiết kế cho một lớp, vì vậy mọi thực hiện của tệp mẫu .h đều có thể '

Do đó, các mẫu không bao giờ được biên dịch riêng biệt và chỉ được biên dịch bất cứ nơi nào bạn có một khởi tạo cụ thể trong một số tệp nguồn khác. Tuy nhiên, việc khởi tạo cụ thể cần biết việc thực hiện tệp mẫu, bởi vì chỉ cần sửa đổitypename Tsử dụng một loại cụ thể trong tệp .h sẽ không thực hiện được công việc bởi vì .cpp có gì để liên kết, tôi không thể tìm thấy nó sau này vì các mẫu nhớ là trừu tượng và không thể được biên dịch, vì vậy tôi bị ép buộc để cung cấp cho việc triển khai ngay bây giờ để tôi biết phải biên dịch và liên kết những gì, và bây giờ tôi có việc triển khai nó được liên kết vào tệp nguồn kèm theo. Về cơ bản, thời điểm tôi khởi tạo một mẫu tôi cần để tạo một lớp hoàn toàn mới và tôi không thể làm điều đó nếu tôi không biết lớp đó trông như thế nào khi sử dụng loại tôi cung cấp trừ khi tôi thông báo cho trình biên dịch triển khai mẫu, vì vậy bây giờ trình biên dịch có thể thay thế Tbằng loại của tôi và tạo một lớp cụ thể sẵn sàng để được biên dịch và liên kết.

Tóm lại, các mẫu là bản thiết kế để xem các lớp sẽ trông như thế nào, các lớp là bản thiết kế cho cách nhìn một đối tượng. Tôi không thể biên dịch các mẫu tách biệt với phần khởi tạo cụ thể của chúng vì trình biên dịch chỉ biên dịch các kiểu cụ thể, nói cách khác, các mẫu ít nhất là trong C ++, là sự trừu tượng hóa ngôn ngữ thuần túy. Chúng ta phải loại bỏ các mẫu trừu tượng để nói và chúng ta làm như vậy bằng cách cung cấp cho chúng một kiểu cụ thể để xử lý để trừu tượng mẫu của chúng ta có thể chuyển đổi thành một tệp lớp thông thường và đến lượt nó, nó có thể được biên dịch bình thường. Việc tách tệp .h mẫu và tệp .cpp mẫu là vô nghĩa. Điều này là vô nghĩa vì việc tách .cpp và .h chỉ là nơi mà .cpp có thể được biên dịch riêng lẻ và được liên kết riêng lẻ, với các mẫu vì chúng tôi không thể biên dịch chúng một cách riêng biệt, vì các mẫu là một sự trừu tượng hóa,

Có nghĩa là typename Tđược thay thế trong bước biên dịch không phải là bước liên kết vì vậy nếu tôi cố gắng biên dịch một mẫu mà không Tbị thay thế như một loại giá trị cụ thể hoàn toàn vô nghĩa đối với trình biên dịch và kết quả là mã đối tượng không thể được tạo bởi vì nó không được tạo biết những gì Tlà.

Về mặt kỹ thuật có thể tạo một số loại chức năng sẽ lưu tệp template.cpp và tắt các loại khi tìm thấy chúng trong các nguồn khác, tôi nghĩ rằng tiêu chuẩn này có một từ khóa exportcho phép bạn đặt các mẫu riêng biệt tập tin cpp nhưng không có nhiều trình biên dịch thực sự thực hiện điều này.

Chỉ cần một lưu ý phụ, khi thực hiện chuyên môn hóa cho một lớp mẫu, bạn có thể tách tiêu đề khỏi triển khai vì chuyên môn theo định nghĩa có nghĩa là tôi chuyên về một loại cụ thể có thể được biên dịch và liên kết riêng lẻ.


4

Một cách để có thực hiện riêng biệt như sau.

//inner_foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>

Internal_foo có các khai báo chuyển tiếp. foo.tpp có triển khai và bao gồm Internal_foo.h; và foo.h sẽ chỉ có một dòng, bao gồm foo.tpp.

Vào thời gian biên dịch, nội dung của foo.h được sao chép vào foo.tpp và sau đó toàn bộ tệp được sao chép vào foo.h sau đó nó sẽ biên dịch. Bằng cách này, không có giới hạn và việc đặt tên là nhất quán, để đổi lấy một tệp bổ sung.

Tôi làm điều này bởi vì các bộ phân tích tĩnh cho mã bị phá vỡ khi nó không thấy các khai báo chuyển tiếp của lớp trong * .tpp. Điều này gây khó chịu khi viết mã trong bất kỳ IDE nào hoặc sử dụng YouCompleteMe hoặc người khác.


2
s / Internal_foo / foo / g và bao gồm foo.tpp ở cuối foo.h. Một tập tin ít hơn.

1

Tôi đề nghị xem trang gcc này thảo luận về sự đánh đổi giữa mô hình "cfront" và "borland" để khởi tạo mẫu.

https://gcc.gnu.org/onlinesocs/gcc-4.6.4/gcc/Template-Instantiation.html

Mô hình "borland" tương ứng với những gì tác giả đề xuất, cung cấp định nghĩa mẫu đầy đủ và có những thứ được biên dịch nhiều lần.

Nó chứa các khuyến nghị rõ ràng liên quan đến việc sử dụng khởi tạo mẫu thủ công và tự động. Ví dụ: tùy chọn "-repo" có thể được sử dụng để thu thập các mẫu cần được khởi tạo. Hoặc một tùy chọn khác là tắt tính năng khởi tạo mẫu tự động bằng cách sử dụng "-fno-ẩn-mẫu" để buộc khởi tạo mẫu thủ công.

Theo kinh nghiệm của tôi, tôi dựa vào các mẫu Thư viện chuẩn và Thư viện C ++ được khởi tạo cho từng đơn vị biên dịch (sử dụng thư viện mẫu). Đối với các lớp mẫu lớn của tôi, tôi thực hiện khởi tạo mẫu thủ công, một lần, cho các loại tôi cần.

Đây là cách tiếp cận của tôi vì tôi đang cung cấp một chương trình làm việc chứ không phải thư viện mẫu để sử dụng trong các chương trình khác. Tác giả của cuốn sách, Josuttis, làm việc rất nhiều trên các thư viện mẫu.

Nếu tôi thực sự lo lắng về tốc độ, tôi cho rằng tôi sẽ khám phá bằng cách sử dụng Tiêu đề được biên dịch sẵn https://gcc.gnu.org/onlinesocs/gcc/Precompiled-Headers.html

đó là đạt được hỗ trợ trong nhiều trình biên dịch. Tuy nhiên, tôi nghĩ rằng các tiêu đề được biên dịch trước sẽ khó khăn với các tệp tiêu đề mẫu.


-2

Một lý do khác là bạn nên viết cả khai báo và định nghĩa trong các tệp tiêu đề là để dễ đọc. Giả sử có một hàm mẫu như vậy trong Utility.h:

template <class T>
T min(T const& one, T const& theOther);

Và trong Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

Điều này đòi hỏi mọi lớp T ở đây phải thực hiện toán tử nhỏ hơn toán tử (<). Nó sẽ đưa ra một lỗi trình biên dịch khi bạn so sánh hai trường hợp lớp chưa triển khai "<".

Do đó, nếu bạn tách riêng khai báo và định nghĩa mẫu, bạn sẽ không thể chỉ đọc tệp tiêu đề để xem phần bên trong của mẫu này để sử dụng API này trên các lớp của riêng bạn, mặc dù trình biên dịch sẽ cho bạn biết điều này trường hợp về toán tử nào cần được ghi đè.


-7

Bạn thực sự có thể định nghĩa lớp mẫu của bạn bên trong tệp .template chứ không phải là tệp .cpp. Bất cứ ai đang nói bạn chỉ có thể định nghĩa nó trong một tệp tiêu đề là sai. Đây là một cái gì đó hoạt động tất cả các cách trở lại c ++ 98.

Đừng quên để trình biên dịch của bạn coi tệp .template của bạn dưới dạng tệp c ++ để giữ ý nghĩa intelli.

Dưới đây là một ví dụ về điều này cho một lớp mảng động.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Bây giờ bên trong tệp .template của bạn, bạn xác định các chức năng của mình như cách bạn thường làm.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }

2
Hầu hết mọi người sẽ định nghĩa một tệp tiêu đề là bất cứ điều gì truyền các định nghĩa đến các tệp nguồn. Vì vậy, bạn có thể đã quyết định sử dụng phần mở rộng tệp ".template" nhưng bạn đã viết một tệp tiêu đề.
Tommy
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.