constexpr, consteval & Lập trình thời gian biên dịch
Di chuyển tính toán từ thời gian chạy sang thời gian biên dịch bằng cách sử dụng constexpr, consteval và constinit. Xây dựng bảng tra cứu, xác thực cấu hình và sử dụng vùng chứa constexpr.
Lập trình thời gian biên dịch trong C++
C++ cho phép bạn chạy mã thực tại thời gian biên dịch. Điều này có nghĩa là trình biên dịch có thể tính toán các giá trị, xác thực dữ liệu đầu vào, xây dựng bảng tra cứu và thậm chí chạy các thuật toán phức tạp — tất cả trước khi chương trình của bạn được triển khai. Kết quả được nhúng trực tiếp vào hệ nhị phân dưới dạng hằng số.
Sự phát triển của lập trình thời gian biên dịch trong C++:
- C++11:constexprđược giới thiệu — các hàm đơn giản, trả về một câu lệnh
- C++14:constexprthoải mái — vòng lặp, biến cục bộ, nhiều câu lệnh
- C++17:if constexpr— phân nhánh tại thời điểm biên dịch để loại bỏ các đường dẫn chưa được sử dụng
- C++20:consteval(phải là thời gian biên dịch),constinit(đảm bảo init tĩnh là thời gian biên dịch), bộ chứaconstexpr(vector,string)
- C++23:if consteval— phát hiện xem bạn có đang ở trong bối cảnh đánh giá liên tục hay không
hàm constexpr
Hàm constexpr có thể được đánh giá tại thời điểm biên dịch hoặc thời gian chạy. Nếu được gọi với các đối số tại thời điểm biên dịch trong ngữ cảnh yêu cầu hằng số (đối số mẫu, biếnconstexpr,static_assert, kích thước mảng), trình biên dịch sẽ đánh giá nó tại thời điểm biên dịch. Nếu không, nó sẽ chạy trong thời gian chạy như một hàm bình thường.
#include <iostream>
#include <array>
#include <cstdint>
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
int next = a + b;
a = b;
b = next;
}
return b;
}
int main() {
// Compile-time evaluation: used in constexpr context
constexpr int fact10 = factorial(10); // computed at compile time
static_assert(fact10 == 3628800);
constexpr int fib10 = fibonacci(10); // computed at compile time
static_assert(fib10 == 55);
// Compile-time array sizes
std::array<int, factorial(5)> arr{}; // array of 120 elements
// Runtime evaluation: argument not constexpr
int n;
std::cin >> n;
std::cout << "factorial(" << n << ") = " << factorial(n) << '\n';
// Same function, now runs at runtime
}quyền lợi và quyền lợi (C++20)
consteval đánh dấu một hàm phải được đánh giá tại thời điểm biên dịch — gọi nó bằng các giá trị thời gian chạy là một lỗi biên dịch. Đây được gọi là "chức năng ngay lập tức." constinit đảm bảo một biến có thời lượng lưu trữ tĩnh hoặc cục bộ theo luồng được khởi tạo tại thời điểm biên dịch, ngăn chặn "lỗi thứ tự khởi tạo tĩnh" — nhưng không giống nhưconstexpr, biến này có thể được sửa đổi sau khi khởi tạo.
#include <iostream>
// consteval: MUST be evaluated at compile time
consteval int square(int x) {
return x * x;
}
// constinit: must be initialized at compile time,
// but can be modified at runtime
constinit int global_value = square(7); // 49, computed at compile time
// constexpr variable: initialized at compile time AND immutable
constexpr int fixed_value = square(10); // 100, compile time, cannot change
int main() {
constexpr int a = square(5); // OK: compile-time context
static_assert(a == 25);
// int x = 5;
// int b = square(x); // ERROR: consteval requires compile-time args
// constinit variable can be modified after initialization
global_value = 100; // OK: runtime modification allowed
std::cout << global_value << '\n'; // 100
// fixed_value = 200; // ERROR: constexpr variable is const
}if constexpr trong thực tế
if constexpr (C++17) đánh giá một điều kiện tại thời điểm biên dịch và loại bỏ toàn bộ nhánh chưa được sử dụng. Nhánh bị loại bỏ không được khởi tạo - nó không cần phải là mã hợp lệ cho các đối số mẫu đã cho. Điều này về cơ bản khác vớiifthông thường, trong đó cả hai nhánh phải hợp lệ.
Các quy tắc chính:
1. Điều kiện phải là biểu thức hằng số thời gian biên dịch.
2. Trong một mẫu, nhánh bị loại bỏ không được khởi tạo cho các đối số mẫu hiện tại.
3. Bên ngoài một mẫu, cả hai nhánh vẫn phải hợp lệ về mặt cú pháp (chúng sẽ không thực thi).
4.if constexprcó thể xuất hiện trong các hàm thông thường, không chỉ trong các mẫu — mà việc loại bỏ chỉ áp dụng trong quá trình khởi tạo mẫu.
5. Mỗi nhánhif constexprcó phạm vi riêng - các biến được khai báo trong một nhánh sẽ không hiển thị ở nhánh kia.
Bộ chứa constexpr (C++20)
C++20 cho phép sử dụngstd::vectorvàstd::stringtrong ngữ cảnh constexpr. Bạn có thể xây dựng, thao tác và truy vấn các vùng chứa tại thời điểm biên dịch — một sự mở rộng đáng kể về khả năng lập trình tại thời gian biên dịch. Ràng buộc: phân bổ tạm thời — bộ nhớ được phân bổ trong quá trình đánh giá thời gian biên dịch phải được giải phóng trước khi quá trình đánh giá constexpr kết thúc.
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <array>
// Build and process a vector at compile time (C++20)
constexpr int sum_of_squares(int n) {
std::vector<int> v;
for (int i = 1; i <= n; ++i) {
v.push_back(i * i);
}
return std::accumulate(v.begin(), v.end(), 0);
// vector memory freed here — transient allocation
}
// Compile-time string processing
constexpr std::size_t count_vowels(std::string_view sv) {
std::size_t count = 0;
for (char c : sv) {
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U') {
++count;
}
}
return count;
}
// Generate a lookup table at compile time
template <std::size_t N>
constexpr auto generate_squares() {
std::array<int, N> result{};
for (std::size_t i = 0; i < N; ++i) {
result[i] = static_cast<int>(i * i);
}
return result;
}
static_assert(sum_of_squares(5) == 55); // 1+4+9+16+25
static_assert(count_vowels("Hello World") == 3);
constexpr auto squares = generate_squares<10>();
static_assert(squares[7] == 49);Lập trình thời gian biên dịch thực tế
Tính toán trong thời gian biên dịch không chỉ là một thủ thuật nhỏ. Các ứng dụng trong thế giới thực bao gồm: xây dựng bảng tra cứu được nhúng trong tệp nhị phân, xác thực cấu hình tại thời điểm biên dịch, tính toán băm để khớp chuỗi tại thời điểm biên dịch và tạo đường dẫn mã được tối ưu hóa.
#include <array>
#include <cstdint>
#include <stdexcept>
// Compile-time CRC32 table generation
constexpr auto generate_crc32_table() {
std::array<uint32_t, 256> table{};
for (uint32_t i = 0; i < 256; ++i) {
uint32_t crc = i;
for (int j = 0; j < 8; ++j) {
crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1));
}
table[i] = crc;
}
return table;
}
constexpr auto crc32_table = generate_crc32_table();
// Compile-time configuration validation
struct Config {
int port;
int max_connections;
int timeout_ms;
};
consteval Config validated_config(int port, int max_conn, int timeout) {
if (port < 1 || port > 65535)
throw "Port out of range"; // compile error if invalid!
if (max_conn < 1 || max_conn > 10000)
throw "Invalid max connections";
if (timeout < 0)
throw "Negative timeout";
return {port, max_conn, timeout};
}
// This validates at compile time — typo or bad value = build fails
constexpr auto config = validated_config(8080, 1000, 5000); // OK
// constexpr auto bad = validated_config(99999, 1000, 5000); // ERROR!Common mistakes with compile-time programming:
1. Assumingconstexprmeans compile-time only — aconstexprfunction can run at runtime too. Onlyconstevalguarantees compile-time evaluation.
2. Transient allocation — in C++20 constexpr contexts, all memory allocated during evaluation must be freed before the evaluation completes. You cannot store aconstexpr std::vectoras a global (its heap memory would persist).
3. Not all functions are constexpr-safe — I/O, thread creation,reinterpret_cast, and inline assembly cannot appear in constexpr contexts.
4. Overusing constexpr — computing everything at compile time increases build times. Reserve it for values that genuinely benefit from being compile-time constants.
constexprfunctions run at compile time OR runtime depending on contextconsteval(C++20) forces compile-time-only evaluation — runtime arguments are compile errorsconstinit(C++20) ensures compile-time initialization of static/thread-local variables but allows runtime modificationif constexprdiscards the untaken branch, which need not be valid for the given template arguments- C++20 allows
std::vectorandstd::stringin constexpr contexts with transient allocation - Các ứng dụng thực tế bao gồm tạo bảng tra cứu, xác thực cấu hình và băm thời gian biên dịch
Câu đố — Kiểm tra kiến thức
(15 XP)1. Sự khác biệt giữa`constexpr`và`consteval`là gì?
2. Bạn có thể đặt`constexpr std::vector<int>`làm biến toàn cục trong C++20 không?
3. `constinit`đảm bảo điều gì?