Lưu trữ định nghĩa hàm mẫu C ++ trong tệp .CPP


526

Tôi có một số mã mẫu mà tôi muốn lưu trữ trong tệp CPP thay vì nội tuyến trong tiêu đề. Tôi biết điều này có thể được thực hiện miễn là bạn biết loại mẫu nào sẽ được sử dụng. Ví dụ:

tập tin .h

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

tập tin .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Lưu ý hai dòng cuối cùng - hàm foo :: do template chỉ được sử dụng với chuỗi ints và std ::, vì vậy những định nghĩa đó có nghĩa là ứng dụng sẽ liên kết.

Câu hỏi của tôi là - đây có phải là một hack khó chịu hay nó sẽ hoạt động với các trình biên dịch / liên kết khác? Hiện tại tôi chỉ sử dụng mã này với VS2008 nhưng sẽ muốn chuyển sang các môi trường khác.


22
Tôi không biết điều này là có thể - một mẹo thú vị! Nó sẽ giúp một số nhiệm vụ gần đây đáng kể để biết điều này - chúc mừng!
xan

69
Điều khiến tôi ngạc nhiên là việc sử dụng donhư một định danh: p
Quentin

tôi đã thực hiện một ngày nào đó tương tự với gcc, nhưng vẫn đang nghiên cứu
Nick

16
Đây không phải là một "hack", nó là sự giải mã về phía trước. Điều này có một vị trí trong tiêu chuẩn của ngôn ngữ; vì vậy, có, nó được cho phép trong mọi trình biên dịch tuân thủ tiêu chuẩn.
Ahmet Ipkin 7/03/2016

1
Điều gì nếu bạn có hàng tá phương pháp? Bạn có thể làm gì template class foo<int>;template class foo<std::string>;ở cuối tập tin .cpp không?
Không biết gì

Câu trả lời:


231

Vấn đề bạn mô tả có thể được giải quyết bằng cách xác định mẫu trong tiêu đề hoặc thông qua cách tiếp cận bạn mô tả ở trên.

Tôi khuyên bạn nên đọc các điểm sau từ C ++ FAQ Lite :

Họ đi sâu vào rất nhiều chi tiết về các vấn đề mẫu này (và khác).


39
Chỉ cần bổ sung cho câu trả lời, liên kết được tham chiếu trả lời câu hỏi một cách tích cực, tức là có thể làm những gì Rob đề xuất và có mã để có thể mang theo.
ivotron

161
Bạn chỉ có thể đăng các phần có liên quan trong câu trả lời chính nó? Tại sao tham chiếu như vậy thậm chí được phép trên SO. Tôi không biết phải tìm gì trong liên kết này vì nó đã được thay đổi rất nhiều kể từ đó.
Nhận dạng

124

Đối với những người khác trên trang này tự hỏi cú pháp chính xác là gì (như tôi) đối với chuyên môn mẫu rõ ràng (hoặc ít nhất là trong VS2008), đây là ...

Trong tệp .h của bạn ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

Và trong tệp .cpp của bạn

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;

15
Bạn có nghĩa là "cho đặc biệt mẫu lớp rõ ràng". Trong trường hợp đó, nó sẽ bao gồm mọi chức năng mà lớp templated có?
Arthur

@Arthur dường như không, tôi có một số phương thức mẫu nằm trong tiêu đề và hầu hết các phương thức khác trong cpp, hoạt động tốt. Giải pháp rất hay.
dùng1633272

Trong trường hợp của người hỏi, họ có một mẫu hàm, không phải mẫu lớp.
dùng253751

23

Mã này được hình thành tốt. Bạn chỉ cần chú ý rằng định nghĩa của mẫu có thể nhìn thấy tại điểm khởi tạo. Để trích dẫn tiêu chuẩn, § 14.7.2.4:

Định nghĩa của mẫu hàm không được xuất, mẫu hàm thành viên không được xuất hoặc hàm thành viên không được xuất hoặc thành viên dữ liệu tĩnh của mẫu lớp sẽ được trình bày trong mọi đơn vị dịch mà nó được khởi tạo rõ ràng.


2
Những gì hiện phi xuất khẩu trung bình?
Dan Nissenbaum

1
@Dan Chỉ hiển thị bên trong đơn vị biên dịch của nó, không hiển thị bên ngoài nó. Nếu bạn liên kết nhiều đơn vị biên dịch lại với nhau, các biểu tượng đã xuất có thể được sử dụng trên chúng (và phải có một hoặc ít nhất là trong trường hợp mẫu, định nghĩa nhất quán, nếu không bạn chạy vào UB).
Konrad Rudolph

Cảm ơn. Tôi nghĩ rằng tất cả các chức năng (theo mặc định) hiển thị bên ngoài đơn vị biên dịch. Nếu tôi có hai đơn vị biên dịch a.cpp(xác định hàm a() {}) và b.cpp(xác định hàm b() { a() }), thì điều này sẽ liên kết thành công. Nếu tôi đúng, thì trích dẫn ở trên dường như không áp dụng cho trường hợp điển hình ... tôi có đang sai ở đâu đó không?
Dan Nissenbaum

@Dan Trivial counterexample: inlinechức năng
Konrad Rudolph

1
Các mẫu hàm @Dan được ngầm định inline. Lý do là nếu không có C ++ ABI được tiêu chuẩn hóa, thật khó / không thể xác định hiệu ứng mà điều này sẽ có.
Konrad Rudolph

15

Điều này sẽ hoạt động tốt ở mọi nơi mẫu được hỗ trợ. Khởi tạo mẫu rõ ràng là một phần của tiêu chuẩn C ++.


13

Ví dụ của bạn là chính xác nhưng không phải là rất di động. Ngoài ra còn có một cú pháp sạch hơn một chút có thể được sử dụng (như được chỉ ra bởi @ namepace-sid).

Giả sử lớp templated là một phần của một số thư viện sẽ được chia sẻ. Các phiên bản khác của lớp templated nên được biên dịch? Là người duy trì thư viện có nghĩa vụ phải dự đoán tất cả các cách sử dụng templated có thể có của lớp?

Một cách tiếp cận thay thế là một biến thể nhỏ trên những gì bạn có: thêm tệp thứ ba là tệp thực hiện / khởi tạo mẫu.

tập tin foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

tập tin foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

tập tin foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

Một lưu ý là bạn cần yêu cầu trình biên dịch biên dịch foo-impl.cppthay vì foo.cppbiên dịch cái sau không làm gì cả.

Tất nhiên, bạn có thể có nhiều triển khai trong tệp thứ ba hoặc có nhiều tệp triển khai cho từng loại bạn muốn sử dụng.

Điều này cho phép linh hoạt hơn nhiều khi chia sẻ lớp templated cho các mục đích sử dụng khác.

Thiết lập này cũng giảm thời gian biên dịch cho các lớp được sử dụng lại vì bạn không biên dịch lại cùng một tệp tiêu đề trong mỗi đơn vị dịch.


cái này mua gì cho bạn Bạn vẫn cần chỉnh sửa foo-impl.cpp để thêm chuyên môn mới.
MK.

Tách các chi tiết triển khai (còn gọi là định nghĩa trong foo.cpp) từ đó phiên bản thực sự được biên dịch (trongfoo-impl.cpp ) và khai báo (in foo.h). Tôi không thích rằng hầu hết các mẫu C ++ được xác định hoàn toàn trong các tệp tiêu đề. Điều đó trái ngược với tiêu chuẩn C / C ++ của các cặp c[pp]/hcho mỗi lớp / không gian tên / bất kỳ nhóm nào bạn sử dụng. Mọi người dường như vẫn sử dụng các tệp tiêu đề nguyên khối đơn giản vì sự thay thế này không được sử dụng rộng rãi hoặc được biết đến.
Cameron Tacklind

1
@MK. Lúc đầu, tôi đã đặt các mẫu tức thời rõ ràng vào cuối định nghĩa trong tệp nguồn cho đến khi tôi cần thêm các lần xuất hiện khác ở nơi khác (ví dụ: kiểm tra đơn vị bằng cách sử dụng giả làm kiểu templated). Sự tách biệt này cho phép tôi thêm nhiều cảnh báo bên ngoài. Hơn nữa, nó vẫn hoạt động khi tôi giữ bản gốc thành một h/cppcặp mặc dù tôi phải bao quanh danh sách ban đầu của các cảnh báo trong một bảo vệ bao gồm, nhưng tôi vẫn có thể biên dịch foo.cppnhư bình thường. Tôi vẫn còn khá mới với C ++ và sẽ rất muốn biết liệu cách sử dụng hỗn hợp này có bất kỳ cảnh báo bổ sung nào không.
Thứ ba

3
Tôi nghĩ rằng nó là tốt hơn để tách rời foo.cppfoo-impl.cpp. Đừng #include "foo.cpp"trong foo-impl.cpptập tin; thay vào đó, hãy thêm khai báo extern template class foo<int>;để foo.cppngăn trình biên dịch khởi tạo mẫu khi biên dịch foo.cpp. Đảm bảo rằng hệ thống xây dựng xây dựng cả hai .cpptệp và chuyển cả hai tệp đối tượng cho trình liên kết. Điều này có nhiều lợi ích: a) rõ ràng trongfoo.cpp chỗ không có sự khởi tạo; b) thay đổi đối với foo.cpp không yêu cầu biên dịch lại foo-impl.cpp.
Shmuel Levine

3
Đây là một cách tiếp cận rất tốt cho vấn đề định nghĩa mẫu tận dụng tốt nhất cả hai thế giới - triển khai tiêu đề và khởi tạo cho các loại thường được sử dụng. Thay đổi duy nhất tôi sẽ thực hiện cho thiết lập này là đổi tên foo.cppthành foo_impl.hfoo-impl.cppthành foo.cpp. Tôi cũng xin thêm typedefs cho sự khởi tạo từ foo.cppđể foo.h, tương tự như vậy using foo_int = foo<int>;. Bí quyết là cung cấp cho người dùng hai giao diện tiêu đề cho sự lựa chọn. Khi người dùng cần khởi tạo được xác định trước, anh ta bao gồm foo.h, khi người dùng cần thứ gì đó không theo thứ tự anh ta bao gồm foo_impl.h.
Wormer

5

Đây chắc chắn không phải là một hack khó chịu, nhưng lưu ý rằng bạn sẽ phải thực hiện nó (chuyên môn mẫu rõ ràng) cho mọi lớp / loại bạn muốn sử dụng với mẫu đã cho. Trong trường hợp NHIỀU loại yêu cầu khởi tạo mẫu, có thể có RẤT NHIỀU dòng trong tệp .cpp của bạn. Để khắc phục vấn đề này, bạn có thể có TemplateClassInst.cpp trong mọi dự án bạn sử dụng để bạn có quyền kiểm soát tốt hơn các loại sẽ được khởi tạo. Rõ ràng giải pháp này sẽ không hoàn hảo (hay còn gọi là viên đạn bạc) vì cuối cùng bạn có thể phá vỡ ODR :).


Bạn có chắc chắn nó sẽ phá vỡ ODR? Nếu các dòng khởi tạo trong TemplateClassInst.cpp tham chiếu đến tệp nguồn giống hệt nhau (chứa các định nghĩa hàm mẫu), thì có đảm bảo không vi phạm ODR không vì tất cả các định nghĩa đều giống nhau (ngay cả khi được lặp lại)?
Dan Nissenbaum

Xin vui lòng, ODR là gì?
không thể di chuyển

4

Có, trong tiêu chuẩn mới nhất, một từ khóa (export ) sẽ giúp giảm bớt vấn đề này, nhưng nó không được triển khai trong bất kỳ trình biên dịch nào mà tôi biết, ngoài Comeau.

Xem FAQ-lite về điều này.


2
AFAIK, xuất khẩu đã chết vì họ đang phải đối mặt với các vấn đề mới hơn và mới hơn, mỗi lần họ giải quyết lần cuối cùng, làm cho giải pháp tổng thể ngày càng phức tạp hơn. Và từ khóa "xuất" sẽ không cho phép bạn "xuất" từ CPP (dù sao vẫn là từ H. Sutter). Vì vậy, tôi nói: Đừng nín thở ...
paercebal

2
Để thực hiện xuất trình biên dịch vẫn yêu cầu định nghĩa mẫu đầy đủ. Tất cả những gì bạn đạt được là có nó ở dạng sắp xếp. Nhưng thực sự không có điểm nào cho nó.
Zan Lynx

2
... Và nó đã đi từ tiêu chuẩn, do sự phức tạp quá mức để đạt được mức tối thiểu.
DevSolar

4

Đó là một cách tiêu chuẩn để xác định các chức năng mẫu. Tôi nghĩ có ba phương pháp tôi đọc để xác định mẫu. Hoặc có lẽ 4. Mỗi người có ưu và nhược điểm.

  1. Xác định trong định nghĩa lớp. Tôi hoàn toàn không thích điều này bởi vì tôi nghĩ định nghĩa lớp học hoàn toàn mang tính tham khảo và nên dễ đọc. Tuy nhiên, việc xác định các mẫu trong lớp ít hơn nhiều so với bên ngoài. Và không phải tất cả các khai báo mẫu đều có cùng mức độ phức tạp. Phương pháp này cũng làm cho mẫu trở thành một mẫu thực sự.

  2. Xác định mẫu trong cùng một tiêu đề, nhưng bên ngoài lớp. Đây là cách ưa thích của tôi hầu hết thời gian. Nó giữ cho định nghĩa lớp của bạn gọn gàng, mẫu vẫn là một mẫu thực sự. Tuy nhiên, nó đòi hỏi phải đặt tên mẫu đầy đủ có thể khó khăn. Ngoài ra, mã của bạn có sẵn cho tất cả. Nhưng nếu bạn cần mã của mình để được nội tuyến thì đây là cách duy nhất. Bạn cũng có thể thực hiện điều này bằng cách tạo tệp .INL ở cuối định nghĩa lớp của bạn.

  3. Bao gồm tiêu đề.h và hiện thực.CPP vào main.CPP của bạn. Tôi nghĩ đó là cách nó được thực hiện. Bạn sẽ không phải chuẩn bị bất kỳ cảnh báo trước nào, nó sẽ hoạt động như một khuôn mẫu thực sự. Vấn đề tôi có với nó là nó không tự nhiên. Chúng tôi thường không bao gồm và mong đợi bao gồm các tệp nguồn. Tôi đoán vì bạn đã bao gồm tệp nguồn, các hàm mẫu có thể được nội tuyến.

  4. Phương pháp cuối cùng này, là cách được đăng, đang xác định các mẫu trong tệp nguồn, giống như số 3; nhưng thay vì bao gồm tệp nguồn, chúng tôi khởi tạo trước các mẫu mà chúng tôi sẽ cần. Tôi không có vấn đề với phương pháp này và đôi khi nó có ích. Chúng tôi có một mã lớn, nó không thể hưởng lợi từ việc được nội tuyến, vì vậy chỉ cần đặt nó vào tệp CPP. Và nếu chúng ta biết các cảnh báo phổ biến và chúng ta có thể xác định trước chúng. Điều này giúp chúng ta tiết kiệm từ việc viết về cơ bản cùng một điều 5, 10 lần. Phương pháp này có lợi ích giữ cho mã của chúng tôi độc quyền. Nhưng tôi không khuyên bạn nên đặt các chức năng nhỏ, được sử dụng thường xuyên trong các tệp CPP. Vì điều này sẽ làm giảm hiệu suất của thư viện của bạn.

Lưu ý, tôi không nhận thức được hậu quả của một tập tin obj cồng kềnh.


3

Vâng, đó là cách tiêu chuẩn để thực hiện khởi tạo chuyên môn rõ ràng. Như bạn đã nêu, bạn không thể khởi tạo mẫu này với các loại khác.

Chỉnh sửa: sửa dựa trên nhận xét.


Không kén chọn thuật ngữ đó là một "khởi tạo rõ ràng".
Richard Corden

2

Hãy lấy một ví dụ, giả sử vì một số lý do bạn muốn có một lớp mẫu:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Nếu bạn biên dịch mã này với Visual Studio - nó sẽ hoạt động tốt. gcc sẽ tạo ra lỗi liên kết (nếu cùng một tệp tiêu đề được sử dụng từ nhiều tệp .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Có thể di chuyển thực hiện sang tệp .cpp, nhưng sau đó bạn cần khai báo lớp như thế này -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

Và sau đó .cpp sẽ trông như thế này:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Không có hai dòng cuối cùng trong tệp tiêu đề - gcc sẽ hoạt động tốt, nhưng Visual studio sẽ tạo ra lỗi:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

cú pháp lớp mẫu là tùy chọn trong trường hợp nếu bạn muốn hiển thị chức năng thông qua xuất khẩu, nhưng điều này chỉ áp dụng cho nền tảng windows - vì vậy test_template.h có thể trông như thế này:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

với tập tin .cpp từ ví dụ trước.

Tuy nhiên, điều này gây đau đầu hơn cho trình liên kết, do đó, nên sử dụng ví dụ trước nếu bạn không xuất hàm.


1

Thời gian để cập nhật! Tạo một tệp nội tuyến (.inl, hoặc có thể là bất kỳ tệp nào khác) và chỉ cần sao chép tất cả các định nghĩa của bạn trong đó. Hãy chắc chắn để thêm mẫu ở trên mỗi chức năng ( template <typename T, ...>). Bây giờ thay vì bao gồm tệp tiêu đề trong tệp nội tuyến, bạn làm ngược lại. Bao gồm tệp nội tuyến sau khi khai báo lớp của bạn (#include "file.inl" ).

Tôi thực sự không biết tại sao không ai đề cập đến điều này. Tôi thấy không có nhược điểm ngay lập tức.


25
Hạn chế ngay lập tức là về cơ bản giống như chỉ xác định các hàm mẫu trực tiếp trong tiêu đề. Khi bạn #include "file.inl", bộ tiền xử lý sẽ dán nội dung file.inltrực tiếp vào tiêu đề. Dù lý do bạn muốn tránh việc triển khai trong tiêu đề, giải pháp này không giải quyết được vấn đề đó.
Cody Grey

5
- có nghĩa là bạn, về mặt kỹ thuật không cần thiết, tự gánh nặng cho mình với nhiệm vụ viết tất cả các bản tóm tắt dài dòng, uốn cong cần thiết theo templateđịnh nghĩa ngoài luồng. Tôi hiểu lý do tại sao mọi người muốn làm điều đó - để đạt được sự tương đương nhất với các khai báo / định nghĩa không phải mẫu, để giữ cho khai báo giao diện trông gọn gàng, v.v. - nhưng không phải lúc nào cũng đáng để phiền phức. Đó là một trường hợp đánh giá sự đánh đổi từ cả hai phía và chọn ra điều tồi tệ nhất . ... cho đến khi namespace classtrở thành một thứ: O [ xin hãy là một thứ ]
underscore_d

2
@Andrew Dường như đã bị mắc kẹt trong đường ống của Ủy ban, mặc dù tôi nghĩ rằng tôi đã thấy ai đó nói rằng đó không phải là cố ý. Tôi ước nó đã biến nó thành C ++ 17. Có thể thập kỷ tới.
gạch dưới

@CodyGray: Về mặt kỹ thuật, điều này thực sự giống với trình biên dịch và do đó nó không làm giảm thời gian biên dịch. Tuy nhiên, tôi nghĩ rằng điều này đáng được đề cập và thực hành trong một số dự án tôi đã thấy. Đi xuống con đường này giúp tách Giao diện khỏi định nghĩa, đó là một thực hành tốt. Trong trường hợp này, nó không giúp tương thích với ABI hoặc tương tự, nhưng nó giúp đọc và hiểu Giao diện.
kiloalphaindia

0

Không có gì sai với ví dụ bạn đã đưa ra. Nhưng tôi phải nói rằng tôi tin rằng việc lưu trữ các định nghĩa hàm trong tệp cpp không hiệu quả. Tôi chỉ hiểu sự cần thiết phải phân tách khai báo và định nghĩa của hàm.

Khi được sử dụng cùng với khởi tạo lớp rõ ràng, Thư viện kiểm tra khái niệm Boost (BCCL) có thể giúp bạn tạo mã chức năng mẫu trong các tệp cpp.


8
Điều gì là không hiệu quả về nó?
Cody Grey

0

Không có cách nào ở trên làm việc cho tôi, vì vậy đây là cách bạn giải quyết nó, lớp tôi chỉ có 1 phương thức templated ..

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

điều này tránh các lỗi liên kết và không cần phải gọi tạm thời

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.