2014-09-01 16 views
28

Tôi đã tạo một số thư viện C++ hiện đang là chỉ tiêu đề. Cả giao diện và việc triển khai các lớp học của tôi đều được viết trong cùng một tệp .hpp.Thiết kế thư viện: cho phép người dùng quyết định giữa "chỉ tiêu đề" và được liên kết động?

Tôi vừa mới bắt đầu nghĩ rằng loại thiết kế không phải là rất tốt:

  1. Nếu người dùng muốn để biên dịch thư viện và liên kết nó tự động, anh/cô ấy không thể.
  2. Thay đổi một dòng mã yêu cầu biên dịch lại đầy đủ các dự án hiện có phụ thuộc vào thư viện.

Tôi thực sự thích các khía cạnh của các thư viện header-chỉ mặc dù: tất cả các chức năng được khả năng inlined và họ đang rất rất dễ dàng để đưa vào dự án của bạn - không cần phải biên dịch/liên kết bất cứ điều gì, chỉ cần một #include chỉ thị đơn giản.

Có thể tận dụng tối đa cả hai thế giới? Ý tôi là - cho phép người dùng chọn cách anh ta muốn sử dụng thư viện. Nó cũng sẽ tăng tốc độ phát triển, khi tôi làm việc trên thư viện trong "chế độ liên kết động" để tránh thời gian biên dịch vô lý và phát hành các sản phẩm đã hoàn thành của tôi trong "chế độ chỉ tiêu đề" để tối đa hóa hiệu suất.

Giao diện và bước phân chia bước hợp lý đầu tiên trong các tệp .hpp.inl.

Tôi không chắc chắn cách tiếp tục. Tôi đã nhìn thấy nhiều thư viện thêm các macro LIBRARY_API vào các khai báo hàm/lớp của chúng - có lẽ điều gì đó tương tự sẽ cần thiết để cho phép người dùng chọn?


EDIT: Tất cả các chức năng thư viện của tôi được bắt đầu với từ khóa inline, để tránh "nhiều định nghĩa về ..." lỗi. Tôi cho rằng từ khóa sẽ được thay thế bằng macro LIBRARY_INLINE trong các tệp .inl? Macro sẽ giải quyết thành inline cho "chế độ chỉ tiêu đề" và không có gì cho "chế độ liên kết động".

+1

Bạn đang nói về loại thư viện nào? Thư viện vùng chứa thường là một tập hợp mẫu và các mẫu này phải là "chỉ tiêu đề". Một thư viện ứng dụng khác nhau. –

+0

@BasileStarynkevitch: Ví dụ: [this] (https://github.com/SuperV1234/SSVUtils) là một trong các thư viện chỉ tiêu đề của tôi. Nó chủ yếu dựa trên mẫu, nhưng cũng có các mô-đun không dựa vào các khuôn mẫu (ví dụ, mô-đun 'CommandLine'). Thay đổi một dòng trong khi phát triển trong một trong các mô-đun không phải mẫu đòi hỏi biên dịch lại đầy đủ. Ngoài ra, thực tế là giao diện và thực hiện không tách biệt làm phiền tôi. Bạn có nghĩ loại thư viện này là một ứng cử viên cho ý tưởng của tôi được mô tả trong câu hỏi không? –

+0

Điểm số 1 của bạn là vô nghĩa. Nếu thư viện là chỉ tiêu đề, tôi không thể liên kết nó, đúng. (Trừ khi tôi viết thư viện wrapper của riêng mình xung quanh nó). Và nếu bạn làm cho nó trở thành một thư viện động, tôi không thể chỉ '# include' nó. Nó cắt giảm cả hai cách. – jalf

Trả lời

14

Lưu ý sơ bộ: Tôi giả sử môi trường cửa sổ, nhưng điều này sẽ dễ dàng chuyển sang môi trường khác.

thư viện của bạn có được chuẩn bị cho bốn tình huống:

  1. Được sử dụng như tiêu đề chỉ thư viện
  2. Được sử dụng như thư viện tĩnh
  3. Được sử dụng như thư viện động (chức năng được nhập khẩu)
  4. Được xây dựng như thư viện động (chức năng được xuất)

Vì vậy, hãy tạo bốn bộ tiền xử lý xác định cho các trường hợp đó: INLINE_LIBRARY, STATIC_LIBRARY, IMPORT_LIBRARYEXPORT_LIBRARY (chỉ là một ví dụ, bạn có thể muốn sử dụng một số lược đồ đặt tên phức tạp). Người dùng phải xác định một trong số họ, tùy thuộc vào những gì anh ta muốn.

Sau đó, bạn có thể viết tiêu đề của bạn như thế này:

// foo.hpp 

#if defined(INLINE_LIBRARY) 
#define LIBRARY_API inline 
#elif defined(STATIC_LIBRARY) 
#define LIBRARY_API 
#elif defined(EXPORT_LIBRARY) 
#define LIBRARY_API __declspec(dllexport) 
#elif defined(IMPORT_LIBRARY) 
#define LIBRARY_API __declspec(dllimport) 
#endif 

LIBRARY_API void foo(); 

#ifdef INLINE_LIBRARY 
#include "foo.cpp" 
#endif 

tập tin thực thi của bạn trông giống như thông thường:

// foo.cpp 

#include "foo.hpp" 
#include <iostream> 

void foo() 
{ 
    std::cout << "foo"; 
} 

Nếu INLINE_LIBRARY được xác định, các chức năng được khai báo inline và thực hiện được bao gồm giống như tệp .inl.

Nếu STATIC_LIBRARY được xác định, các hàm được khai báo mà không có bất kỳ trình chỉ định nào, người dùng phải đưa tệp .cpp vào quá trình xây dựng của mình.

Nếu IMPORT_LIBRARY được xác định, các hàm được nhập, không cần thực hiện bất kỳ.

Nếu EXPORT_LIBRARY được xác định, các hàm được xuất và người dùng phải biên dịch các tệp .cpp đó.

Chuyển đổi giữa tĩnh/nhập/xuất là một điều thực sự phổ biến, nhưng tôi không chắc chắn nếu thêm tiêu đề chỉ vào phương trình là một điều tốt. Thông thường, có một lý do chính đáng để xác định nội dung nào đó hoặc không làm như vậy. Cá nhân, tôi thích đặt mọi thứ vào các tệp .cpp trừ khi nó thực sự phải được inline (như các mẫu) hoặc nó có ý nghĩa về hiệu năng (các hàm rất nhỏ, thường là một lớp lót). Điều này làm giảm cả thời gian biên dịch và cách thức phụ thuộc nhiều hơn.

Nhưng nếu tôi chọn xác định nội tuyến nào đó, tôi luôn đặt nó trong các tệp .inl riêng biệt, chỉ để giữ cho các tệp tiêu đề sạch sẽ và dễ hiểu.

+0

Điều này có vẻ là một cách tiếp cận rất tốt và sạch sẽ. Một số câu hỏi nhỏ trước khi chấp nhận nó: bạn có nghĩ rằng có bất kỳ hạn chế nào khi sử dụng phương pháp này không? Tôi có nên gọi các tệp triển khai .inl hoặc .cpp không? (Đây là một câu hỏi chủ yếu, nhưng bạn nghĩ gì? Có lẽ mẫu thực hiện trong .inl và nguồn trong .cpp?) –

+0

@Vittorio - Giống như tôi đã viết, đó là cách tiếp cận chung để chuyển đổi giữa liên kết tĩnh và động. Tôi không bao giờ sử dụng nó cho nội tuyến có điều kiện, mặc dù. Thoạt nhìn, tôi không thấy bất kỳ hạn chế nào trong chính cơ chế đó, nhưng có thể nó sẽ làm cho một nội dung trở thành nội tuyến không nên được gạch chân và ngược lại. – Horstling

+0

Về việc đặt tên: .cpp có thể gây ra một số nhầm lẫn nếu nó được đưa vào, nhưng tôi sẽ đặt trước .inl cho các công cụ được đưa vào vô điều kiện (như định nghĩa mẫu). – Horstling

4

Đây là hệ điều hành và trình biên dịch cụ thể. Trên Linux với trình biên dịch GCC rất gần đây (phiên bản 4.9), bạn có thể tạo một thư viện tĩnh tĩnh sử dụng interprocedural linktime optimization.

Điều này có nghĩa là bạn xây dựng thư viện của mình với g++ -O2 -flto cả lúc biên dịch và tại thời gian liên kết thư viện và bạn sử dụng thư viện của mình với g++ -O2 -flto cả lúc biên dịch và liên kết thời gian của chương trình gọi.

2

Lý do Đặt càng ít càng cần thiết trong các tệp tiêu đề và càng nhiều càng tốt trong các mô-đun thư viện, vì những lý do bạn đã đề cập: thời gian biên dịch và thời gian biên dịch dài. Lý do chính đáng cho các mô-đun chỉ tiêu đề là:

  1. mẫu chung cho thông số mẫu do người dùng xác định;

  2. chức năng tiện lợi rất ngắn khi nội tuyến cho hiệu suất đáng kể .

Trong trường hợp 1, thường có thể ẩn một số chức năng không phụ thuộc vào loại do người dùng xác định trong tệp .cpp.

Kết luận Nếu bạn tuân theo lý do này, thì không có lựa chọn: chức năng khuôn mẫu phải cho phép người dùng xác định loại không thể được biên dịch trước, nhưng yêu cầu triển khai chỉ tiêu đề. Chức năng khác cần được ẩn khỏi người dùng trong thư viện để tránh hiển thị chúng cho các chi tiết triển khai.

1

Thay vì thư viện động, bạn có thể có thư viện tĩnh tĩnh và tệp tiêu đề mỏng. Trong một bản dựng nhanh tương tác, bạn sẽ có được lợi ích khi không phải biên dịch lại thế giới nếu chi tiết triển khai thay đổi. Nhưng một bản phát hành được tối ưu hóa hoàn toàn có thể thực hiện tối ưu hóa toàn cầu và vẫn tìm ra nó có thể có các hàm nội dòng. Về cơ bản, với "Link Time Code Generation", bộ công cụ thực hiện thủ thuật mà bạn đang nghĩ đến.

Tôi quen thuộc với trình biên dịch của Microsoft, mà tôi biết chắc chắn thực hiện điều này như của VS10 (nếu không sớm hơn).

1

Mã mẫu sẽ nhất thiết phải là tiêu đề chỉ: để khởi tạo mã này, các tham số kiểu phải được biết tại thời gian biên dịch. Không có cách nào để nhúng mã mẫu trong các thư viện được chia sẻ. Chỉ .NET và Java hỗ trợ JIT instantiation từ byte-code.

Re: mã không phải mẫu, đối với một lớp lót ngắn, tôi khuyên bạn chỉ nên giữ tiêu đề. Các hàm nội tuyến cung cấp cho trình biên dịch nhiều cơ hội hơn để tối ưu hóa mã cuối cùng.

Để tránh "thời gian biên dịch điên", MS Visual C có tính năng "được biên dịch trước". Tôi không nghĩ GCC có tính năng tương tự.

Chức năng dài không được đặt trong bất kỳ trường hợp nào.

Tôi đã có một dự án có bit chỉ tiêu đề, bit thư viện được biên dịch và một số bit tôi không thể quyết định vị trí của chúng. Tôi đã có tệp .inc, có điều kiện được bao gồm trong .hpp hoặc .cxx tùy thuộc vào #ifdef. Sự thật cần phải nói, dự án luôn được biên dịch ở chế độ "nội tuyến tối đa", vì vậy sau một thời gian tôi đã loại bỏ các tệp .inc và chỉ cần di chuyển nội dung sang tệp .hpp.

+0

GCC có PCH kể từ 3.4 (2006). – alecov

2

Điều này là để bổ sung cho câu trả lời của @ Horstling.


Bạn có thể tạo ra một tĩnh hoặc một động thư viện. Khi bạn tạo các thư viện liên kết tĩnh, mã được biên dịch cho tất cả các hàm/đối tượng sẽ được lưu vào một tệp (với phần mở rộng .lib trong Windows). Tại dự án chính của dự án (thời gian sử dụng thư viện), các mã này sẽ được liên kết vào tệp thực thi cuối cùng của bạn cùng với các mã dự án chính. Vì vậy, thực thi cuối cùng sẽ không có bất kỳ sự phụ thuộc thời gian chạy nào.

Thư viện được liên kết động sẽ được hợp nhất vào dự án chính vào thời gian chạy (và không phải thời gian liên kết). Khi bạn biên dịch thư viện, bạn nhận được một tệp .dll (chứa mã được biên dịch thực tế) và tệp .lib (chứa đủ dữ liệu cho trình biên dịch/thời gian chạy để tìm các hàm/đối tượng trong tệp .dll). Tại thời gian liên kết, tệp thực thi sẽ được định cấu hình để tải .dll và sử dụng mã được biên dịch từ .dll đó khi cần. Bạn sẽ cần phải phân phối các tập tin .dll với thực thi của bạn để có thể chạy nó.

Không cần phải chọn giữa liên kết tĩnh hoặc động (hoặc chỉ tiêu đề) khi thiết kế thư viện của bạn, bạn tạo nhiều dự án/makefiles, một để tạo một tệp .lib tĩnh, một để tạo tệp .lib/.dll ghép đôi và phân phối cả hai phiên bản, để người dùng lựa chọn giữa. (Bạn sẽ cần phải sử dụng các macro tiền xử lý như macro @Horstling được đề xuất).


Bạn không thể đặt bất kỳ mẫu trong thư viện trước khi biên soạn, trừ khi bạn sử dụng một kỹ thuật gọi là Explicit õ, làm hạn chế các thông số mẫu.

Cũng lưu ý rằng trình biên dịch/liên kết hiện đại thường không tôn trọng công cụ sửa đổi nội tuyến. Họ có thể nội tuyến một chức năng ngay cả khi nó không được chỉ định là nội tuyến, hoặc có thể tự động gọi một người khác có sửa đổi nội tuyến, khi họ thấy phù hợp. (Bất kể, tôi sẽ tư vấn đặt nội tuyến một cách rõ ràng khi áp dụng cho khả năng tương thích tối đa). Vì vậy, sẽ không có bất kỳ hình phạt hiệu suất thời gian chạy nào nếu bạn sử dụng thư viện được liên kết tĩnh thay vì thư viện chỉ tiêu đề (và cho phép tối ưu hóa trình biên dịch/liên kết). Như những người khác đã đề xuất, đối với các chức năng thực sự nhỏ chắc chắn được hưởng lợi từ việc được gọi là nội dòng, cách tốt nhất là đặt chúng vào tệp tiêu đề, do đó thư viện được liên kết động cũng sẽ không bị mất hiệu suất đáng kể. (Trong mọi trường hợp, các chức năng nội tuyến sẽ chỉ ảnh hưởng đến hiệu suất cho các hàm được gọi rất thường xuyên, các vòng lặp bên trong sẽ được gọi là hàng nghìn/triệu lần).


Thay vì đặt các chức năng nội tuyến trong các tập tin tiêu đề (với một #include "foo.cpp" trong tiêu đề của bạn), bạn có thể thay đổi makefile/thiết lập dự án và thêm Foo.cpp vào danh sách các file nguồn được biên dịch. Bằng cách này, nếu bạn thay đổi bất kỳ việc thực hiện chức năng nào sẽ không cần phải biên dịch lại toàn bộ dự án và chỉ foo.cpp mới được biên dịch lại. Như tôi đã đề cập trước đó, các chức năng nhỏ của bạn sẽ vẫn được biên dịch bởi trình biên dịch tối ưu hóa, và bạn không cần phải lo lắng về điều đó.


Nếu bạn sử dụng/thiết kế thư viện được biên dịch trước, bạn nên xem xét trường hợp thư viện được biên dịch với một phiên bản trình biên dịch khác cho dự án chính. Mỗi phiên bản trình biên dịch khác nhau (thậm chí là các cấu hình khác nhau, như Debug hoặc Release) sử dụng thời gian chạy C khác nhau (như memcpy, printf, fopen, ...) và thời gian chạy thư viện chuẩn C++ (như std :: vector <>, std :: chuỗi, ...). Các triển khai thư viện khác nhau này có thể làm phức tạp liên kết, hoặc thậm chí tạo ra các lỗi thời gian chạy.

Theo nguyên tắc chung, luôn tránh chia sẻ đối tượng thời gian chạy trình biên dịch (cấu trúc dữ liệu không được định nghĩa theo tiêu chuẩn, như FILE *) trên thư viện, vì cấu trúc dữ liệu không tương thích sẽ dẫn đến lỗi thời gian chạy.

Khi liên kết dự án của bạn, các hàm thời gian chạy C/C++ phải được liên kết với thư viện của bạn .lib hoặc .lib/.dll hoặc tệp .exe của bạn. Bản thân thời gian chạy C/C++ có thể được liên kết như là thư viện tĩnh hoặc động (bạn có thể thiết lập nó trong các thiết lập makefile/project).

Bạn sẽ thấy liên kết động với thời gian chạy C/C++ trong cả thư viện và dự án chính (ngay cả khi bạn biên dịch thư viện như thư viện tĩnh) tránh hầu hết các sự cố liên kết (với các triển khai hàm trùng lặp trong nhiều phiên bản thời gian chạy) . Tất nhiên bạn sẽ cần phải phân phối thời gian chạy DLL cho tất cả các phiên bản sử dụng với thực thi của bạn và thư viện.

Có các trường hợp liên kết tĩnh với thời gian chạy C/C++, và cách tốt nhất trong các trường hợp này là biên dịch thư viện với cài đặt trình biên dịch giống như dự án chính để tránh các vấn đề liên kết.

+0

Bạn có thể linh hoạt hơn về điểm đặt các khuôn mẫu trong thư viện dựng sẵn. Trước tiên, bạn có thể nhanh chóng khởi tạo để đưa những người bạn biết là cần thiết vào thư viện được chia sẻ và ngăn không cho chúng được khởi tạo cho các loại đó nhưng biết liên kết với chúng, khi mã sử dụng mẫu. Thứ hai, bạn có thể đặt các phần tử chức năng vào một tệp .h khác và chỉ bao gồm tệp đó nếu bạn cần loại chưa được biên dịch trước. Đây là cách di động cũ để ngăn chặn sự khởi tạo. Nó có thể ngăn cản việc kéo thêm phụ thuộc vào tiêu đề (chỉ delcaration). –

0

Có thể tận dụng tối đa cả hai thế giới?

Về mặt; hạn chế phát sinh bởi vì các công cụ không đủ thông minh.This answer cho nỗ lực hiện tại tốt nhất vẫn đủ di động để sử dụng hiệu quả.

Gần đây tôi đã bắt đầu nghĩ rằng loại thiết kế này không phải là rất tốt.

Nó phải là. Thư viện chỉ tiêu đề là lý tưởng vì chúng đơn giản hóa việc triển khai: làm cho cơ chế sử dụng lại ngôn ngữ tương tự như hầu như tất cả những thứ khác ', đó chỉ là điều lành mạnh để làm. Nhưng đây là C++. Các công cụ C++ hiện tại vẫn dựa trên các mô hình liên kết nửa thế kỷ cũ, loại bỏ các mức độ linh hoạt quan trọng, chẳng hạn như chọn các điểm nhập để nhập hoặc xuất ở một mức riêng lẻ mà không cần buộc phải thay đổi mã nguồn ban đầu của thư viện. Ngoài ra, C++ thiếu một hệ thống mô-đun thích hợp và vẫn dựa vào các hoạt động sao chép-dán được tôn vinh để hoạt động (mặc dù đây chỉ là một yếu tố phụ cho vấn đề được đề cập).

Thực tế, MSVC tốt hơn một chút về vấn đề này. Đây là triển khai chính duy nhất cố gắng đạt được một mức độ mô đun nào đó trong C++ (bằng cách thử ví dụ: C++ modules). Và nó là trình biên dịch duy nhất thực sự cho phép ví dụ như sau:

//// Module.c++ 
#pragma once 
inline void Func() { /* ... */ } 

//// Program1.c++ 
#include <Module.c++> 
// Inlines or "vague" links Func(), whatever is better. 
int main() { Func(); } 

//// Program2.c++ 
// This forces Func() to be imported. 
// The declaration must come *BEFORE* the definition. 
__declspec(dllimport) __declspec(noinline) void Func(); 
#include <Module.c++> 
int main() { Func(); } 

//// Program3.c++ 
// This forces Func() to be exported. 
__declspec(dllexport) __declspec(noinline) void Func(); 
#include <Module.c++> 

Lưu ý rằng điều này có thể được sử dụng để nhập khẩu có chọn lọc và xuất các biểu tượng riêng lẻ từ thư viện, mặc dù vẫn cồng kềnh.

GCC cũng chấp nhận điều này (nhưng thứ tự của các tờ khai phải được thay đổi) và Clang không có bất kỳ cách nào để đạt được hiệu quả tương tự mà không thay đổi nguồn của thư viện.

Các vấn đề liên quan