Thuyết minh rõ ràng cho phép giảm thời gian biên dịch và kích thước đối tượng
Đây là những lợi ích chính mà nó có thể cung cấp. Chúng đến từ hai hiệu ứng sau được mô tả chi tiết trong các phần bên dưới:
- xóa các định nghĩa khỏi tiêu đề để ngăn các công cụ xây dựng xây dựng lại các bao gồm
- định nghĩa lại đối tượng
Xóa định nghĩa khỏi tiêu đề
Thuyết minh rõ ràng cho phép bạn để lại các định nghĩa trong tệp .cpp.
Khi định nghĩa nằm trên tiêu đề và bạn sửa đổi nó, một hệ thống xây dựng thông minh sẽ biên dịch lại tất cả các tệp bao gồm, có thể là hàng chục tệp, làm cho việc biên dịch chậm đến mức khó chịu.
Việc đặt định nghĩa trong tệp .cpp có nhược điểm là các thư viện bên ngoài không thể sử dụng lại mẫu với các lớp mới của riêng chúng, nhưng "Xóa định nghĩa khỏi các tiêu đề được bao gồm nhưng cũng hiển thị các mẫu một API bên ngoài" bên dưới cho thấy một giải pháp.
Xem ví dụ cụ thể bên dưới.
Lợi ích khi xác định lại đối tượng: hiểu được vấn đề
Nếu bạn chỉ xác định hoàn toàn một mẫu trên tệp tiêu đề, mọi đơn vị biên dịch bao gồm tiêu đề đó sẽ kết thúc việc biên dịch bản sao ngầm của chính nó về mẫu cho mọi cách sử dụng đối số mẫu khác nhau được thực hiện.
Điều này có nghĩa là rất nhiều thời gian biên dịch và sử dụng đĩa vô ích.
Dưới đây là một ví dụ cụ thể, trong đó xác định cả main.cpp
và notmain.cpp
ngầm định MyTemplate<int>
do việc sử dụng nó trong các tệp đó.
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
#endif
notmain.hpp
#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP
int notmain();
#endif
GitHub ngược dòng .
Biên dịch và xem các ký hiệu với nm
:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate
Đầu ra:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
Từ đó man nm
, chúng tôi thấy điều đó W
có nghĩa là biểu tượng yếu, GCC đã chọn vì đây là một hàm mẫu. Biểu tượng yếu nghĩa là mã được tạo ngầm định MyTemplate<int>
đã biên dịch cho được biên dịch trên cả hai tệp.
Lý do nó không hoạt động tại thời điểm liên kết với nhiều định nghĩa là trình liên kết chấp nhận nhiều định nghĩa yếu và chỉ chọn một trong số chúng để đưa vào tệp thực thi cuối cùng.
Các số trong đầu ra có nghĩa là:
0000000000000000
: địa chỉ trong phần. Số không này là do các mẫu được tự động đưa vào phần riêng của chúng
0000000000000017
: kích thước của mã được tạo cho chúng
Chúng ta có thể thấy điều này rõ ràng hơn một chút với:
objdump -S main.o | c++filt
kết thúc bằng:
Disassembly of section .text._ZN10MyTemplateIiE1fEi:
0000000000000000 <MyTemplate<int>::f(int)>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 89 75 f4 mov %esi,-0xc(%rbp)
f: 8b 45 f4 mov -0xc(%rbp),%eax
12: 83 c0 01 add $0x1,%eax
15: 5d pop %rbp
16: c3 retq
và _ZN10MyTemplateIiE1fEi
là tên đọc sai của MyTemplate<int>::f(int)>
mà c++filt
quyết định không unmangle.
Vì vậy, chúng ta thấy rằng một phần riêng biệt được tạo ra cho mỗi lần khởi tạo phương thức đơn lẻ và mỗi phần trong số chúng tất nhiên sẽ chiếm không gian trong các tệp đối tượng.
Giải pháp cho vấn đề xác định lại đối tượng
Vấn đề này có thể tránh được bằng cách sử dụng trình diễn thuyết rõ ràng và:
giữ định nghĩa trên hpp và thêm extern template
vào hpp cho các loại sẽ được khởi tạo một cách rõ ràng.
Như đã giải thích tại: việc sử dụng mẫu extern (C ++ 11) extern template
ngăn không cho một mẫu đã xác định hoàn toàn được khởi tạo bởi các đơn vị biên dịch, ngoại trừ việc khởi tạo rõ ràng của chúng tôi. Bằng cách này, chỉ phần khởi tạo rõ ràng của chúng tôi sẽ được xác định trong các đối tượng cuối cùng:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
extern template class MyTemplate<int>;
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
Nhược điểm:
- nếu bạn là thư viện chỉ tiêu đề, bạn buộc các dự án bên ngoài thực hiện việc khởi tạo rõ ràng của riêng chúng. Nếu bạn không phải là một thư viện chỉ có tiêu đề, giải pháp này có thể là tốt nhất.
- nếu loại mẫu được xác định trong dự án của riêng bạn và không phải là loại tích hợp sẵn
int
, có vẻ như bạn buộc phải thêm bao gồm cho nó trên tiêu đề, một khai báo chuyển tiếp là không đủ: mẫu extern & loại không đầy đủ Điều này làm tăng sự phụ thuộc của tiêu đề một chút.
di chuyển định nghĩa trên tệp cpp, chỉ để lại khai báo trên hpp, tức là sửa đổi ví dụ ban đầu thành:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
template class MyTemplate<int>;
Nhược điểm: các dự án bên ngoài không thể sử dụng mẫu của bạn với các loại riêng của chúng. Ngoài ra, bạn buộc phải khởi tạo rõ ràng tất cả các loại. Nhưng có lẽ đây là một sự ngược lại mà từ đó các lập trình viên sẽ không quên.
giữ nguyên định nghĩa trên hpp và thêm extern template
vào mọi bao gồm:
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int notmain() { return MyTemplate<int>().f(1); }
Nhược điểm: tất cả các bao gồm phải thêm extern
vào tệp CPP của họ, điều mà các lập trình viên có thể sẽ quên làm.
Với một trong hai giải pháp đó, nm
bây giờ chứa:
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
vì vậy chúng tôi thấy chỉ mytemplate.o
có một biên dịch MyTemplate<int>
như mong muốn, trong khi notmain.o
và main.o
không bởi vì U
có nghĩa là không xác định.
Xóa định nghĩa khỏi các tiêu đề được bao gồm nhưng cũng hiển thị các mẫu một API bên ngoài trong thư viện chỉ dành cho tiêu đề
Nếu thư viện của bạn không chỉ có tiêu đề, extern template
phương pháp sẽ hoạt động, vì việc sử dụng các dự án sẽ chỉ liên kết đến tệp đối tượng của bạn, tệp này sẽ chứa đối tượng của bản khởi tạo mẫu rõ ràng.
Tuy nhiên, đối với thư viện chỉ dành cho tiêu đề, nếu bạn muốn cả hai:
- tăng tốc độ biên dịch dự án của bạn
- hiển thị tiêu đề như một API thư viện bên ngoài để người khác sử dụng nó
thì bạn có thể thử một trong những cách sau:
-
mytemplate.hpp
: định nghĩa mẫu
mytemplate_interface.hpp
: khai báo mẫu chỉ khớp với các định nghĩa từ mytemplate_interface.hpp
, không có định nghĩa
mytemplate.cpp
: bao gồm mytemplate.hpp
và thực hiện các đánh giá rõ ràng
main.cpp
và ở mọi nơi khác trong cơ sở mã: bao gồm mytemplate_interface.hpp
, khôngmytemplate.hpp
-
mytemplate.hpp
: định nghĩa mẫu
mytemplate_implementation.hpp
: bao gồm mytemplate.hpp
và thêm extern
vào mọi lớp sẽ được khởi tạo
mytemplate.cpp
: bao gồm mytemplate.hpp
và thực hiện các đánh giá rõ ràng
main.cpp
và ở mọi nơi khác trong cơ sở mã: bao gồm mytemplate_implementation.hpp
, khôngmytemplate.hpp
Hoặc thậm chí tốt hơn có lẽ cho nhiều tiêu đề: tạo một thư mục intf
/ impl
bên trong includes/
thư mục của bạn và sử dụng mytemplate.hpp
làm tên luôn luôn.
Cách mytemplate_interface.hpp
tiếp cận trông như thế này:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
#include "mytemplate_interface.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
#endif
mytemplate_interface.hpp
#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate_interface.hpp"
int main() {
std::cout << MyTemplate<int>().f(1) << std::endl;
}
Biên dịch và chạy:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o
Đầu ra:
2
Đã thử nghiệm trong Ubuntu 18.04.
C ++ 20 mô-đun
https://en.cppreference.com/w/cpp/language/modules
Tôi nghĩ rằng tính năng này sẽ cung cấp thiết lập tốt nhất về sau khi nó có sẵn, nhưng tôi chưa kiểm tra nó vì nó chưa có trên GCC 9.2.1 của tôi.
Bạn vẫn sẽ phải khởi tạo rõ ràng để tăng tốc độ / tiết kiệm đĩa, nhưng ít nhất chúng tôi sẽ có một giải pháp lành mạnh cho "Xóa định nghĩa khỏi các tiêu đề được bao gồm nhưng cũng hiển thị các mẫu API bên ngoài" mà không yêu cầu sao chép mọi thứ khoảng 100 lần.
Việc sử dụng mong đợi (không có chú thích rõ ràng, không chắc chắn cú pháp chính xác sẽ như thế nào, hãy xem: Cách sử dụng mẫu thuyết minh rõ ràng với các mô-đun C ++ 20? ) Là một cái gì đó cùng:
helloworld.cpp
export module helloworld;
import <iostream>;
template<class T>
export void hello(T t) {
std::cout << t << std::end;
}
main.cpp
import helloworld;
int main() {
hello(1);
hello("world");
}
và sau đó biên dịch được đề cập tại https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/
clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o
Vì vậy, từ điều này chúng ta thấy rằng clang có thể trích xuất giao diện mẫu + thực hiện thành phép thuật helloworld.pcm
, trong đó phải chứa một số đại diện trung gian LLVM của nguồn: Các mẫu được xử lý như thế nào trong hệ thống mô-đun C ++? mà vẫn cho phép đặc tả mẫu xảy ra.
Cách nhanh chóng phân tích bản dựng của bạn để xem liệu nó có thu được nhiều lợi ích từ việc tạo mẫu hay không
Vì vậy, bạn có một dự án phức tạp và bạn muốn quyết định xem việc khởi tạo mẫu có mang lại lợi nhuận đáng kể mà không thực sự thực hiện cấu trúc lại đầy đủ hay không?
Phân tích bên dưới có thể giúp bạn quyết định hoặc ít nhất chọn các đối tượng hứa hẹn nhất để cấu trúc lại trước khi bạn thử nghiệm, bằng cách mượn một số ý tưởng từ: Tệp đối tượng C ++ của tôi quá lớn
# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
grep ' W ' > nm.log
# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log
# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log
# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log
# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list.
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
sort -k1 -n > nm.gains.log
# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log
# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log
Giấc mơ: một bộ đệm trình biên dịch mẫu
Tôi nghĩ giải pháp cuối cùng sẽ là nếu chúng ta có thể xây dựng với:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
và sau đó myfile.o
sẽ tự động sử dụng lại các mẫu đã biên dịch trước đó trên các tệp.
Điều này có nghĩa là các lập trình viên phải nỗ lực thêm 0 bên cạnh việc chuyển tùy chọn CLI bổ sung đó vào hệ thống xây dựng của bạn.
Phần thưởng phụ của việc khởi tạo mẫu rõ ràng: trợ giúp IDEs liệt kê các bản khởi tạo mẫu
Tôi nhận thấy rằng một số IDE chẳng hạn như Eclipse không thể giải quyết "danh sách tất cả các khởi tạo mẫu được sử dụng".
Vì vậy, ví dụ: nếu bạn đang ở bên trong một mã được tạo mẫu và bạn muốn tìm các giá trị có thể có của mẫu, bạn sẽ phải tìm từng cách sử dụng của hàm tạo và suy ra từng kiểu có thể có.
Nhưng trên Eclipse 2020-03, tôi có thể dễ dàng liệt kê các mẫu được khởi tạo một cách rõ ràng bằng cách thực hiện tìm kiếm Tìm tất cả các cách sử dụng (Ctrl + Alt + G) trên tên lớp, ví dụ:
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
đến:
template class AnimalTemplate<Dog>;
Đây là bản demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Tuy nhiên, một kỹ thuật du kích khác mà bạn có thể sử dụng bên ngoài IDE sẽ là chạy nm -C
trên tệp thực thi cuối cùng và ghi tên mẫu:
nm -C main.out | grep AnimalTemplate
trực tiếp chỉ ra thực tế Dog
là một trong những cách diễn đạt:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)