哈喽,各位好!今天咱们来聊聊C++20里那些constexpr骚操作,尤其是怎么在编译期玩转std::string
和std::vector
。这玩意儿听起来挺高大上,但其实一旦掌握了,能让你的代码跑得飞起,还能提前发现一堆bug。
开场白:constexpr是什么鬼?
首先,咱们得搞清楚constexpr
是个什么东西。简单来说,constexpr
就是告诉编译器:“哥们儿,这个函数(或者变量)你给我老老实实在编译期算出来!别等到运行的时候再磨磨唧唧的。”
这样做的好处可多了:
- 性能提升: 编译期就算好了,运行的时候直接用,速度当然快。
- 编译期检查: 很多错误可以在编译期就发现,不用等到上线了才炸。
- 模板元编程: 配合模板,能玩出更多花样,实现一些神奇的功能。
constexpr std::string:字符串的编译期魔术
在C++11/14/17的时候,std::string
想成为constexpr
,那简直是难于上青天。但是C++20给了我们希望!虽然不是所有的std::string
操作都能在编译期完成,但至少我们能做一些有意思的事情了。
限制:
- 动态内存分配:
std::string
底层是用动态内存分配的,所以C++20对constexpr std::string
有严格的限制,主要是避免编译期出现动态内存分配。 - 操作限制:不是所有
std::string
的方法都能在constexpr
上下文中使用。
基本用法:
#include <string>
#include <iostream>
constexpr std::string compile_time_string = "Hello, constexpr string!";
int main() {
std::cout << compile_time_string << std::endl;
return 0;
}
这个例子很简单,定义了一个constexpr std::string
,然后在运行时打印出来。但这仅仅是冰山一角。
拼接字符串:
#include <string>
#include <iostream>
constexpr std::string concatenate(const char* str1, const char* str2) {
std::string result;
for (size_t i = 0; str1[i] != ''; ++i) {
result += str1[i];
}
for (size_t i = 0; str2[i] != ''; ++i) {
result += str2[i];
}
return result;
}
int main() {
constexpr std::string combined = concatenate("Hello, ", "World!");
std::cout << combined << std::endl;
return 0;
}
注意: 上面的代码在C++20下无法编译通过,因为在constexpr
函数里动态增长std::string
是不允许的。我们需要使用一些技巧来绕过这个限制。比如,我们可以预先分配足够的空间,或者使用std::array
和std::string_view
来模拟字符串操作。
正确的constexpr字符串拼接 (C++20):
#include <string_view>
#include <array>
#include <iostream>
template <size_t N1, size_t N2>
constexpr auto concatenate(const char (&str1)[N1], const char (&str2)[N2]) {
std::array<char, N1 + N2 - 1> result{}; // -1 因为两个字符串都有null terminator
size_t i = 0;
for (; i < N1 - 1; ++i) {
result[i] = str1[i];
}
for (size_t j = 0; j < N2 - 1; ++j, ++i) {
result[i] = str2[j];
}
result[i] = ''; // 保证null termination
return result;
}
int main() {
constexpr auto combined = concatenate("Hello, ", "World!");
std::cout << combined.data() << std::endl;
return 0;
}
这个例子使用std::array
来存储结果,避免了动态内存分配。concatenate
函数使用模板参数推导字符串的长度,并在编译期计算出结果数组的大小。 combined.data()
返回底层字符数组的指针。
字符串查找:
#include <string_view>
#include <iostream>
constexpr size_t find_char(std::string_view str, char c) {
for (size_t i = 0; i < str.size(); ++i) {
if (str[i] == c) {
return i;
}
}
return std::string_view::npos;
}
int main() {
constexpr std::string_view my_string = "This is a test string.";
constexpr size_t pos = find_char(my_string, 't');
if (pos != std::string_view::npos) {
std::cout << "Found 't' at position: " << pos << std::endl;
} else {
std::cout << "'t' not found." << std::endl;
}
return 0;
}
constexpr std::vector:编译期容器的逆袭
std::vector
和std::string
面临着类似的问题:动态内存分配。要在编译期使用std::vector
,我们需要一些特殊的技巧。C++20对constexpr
函数的要求更高,限制更多,但也更加强大。
限制:
- 动态内存分配:同
std::string
,编译期std::vector
要避免动态内存分配。 - 构造函数:并非所有构造函数都可以在
constexpr
上下文中使用。
基本用法:
直接声明constexpr std::vector
通常不可行,因为std::vector
的构造函数涉及到动态内存分配。我们需要使用一些变通方法,比如使用std::array
或者自定义的静态数组类。
使用std::array
模拟constexpr std::vector
:
#include <array>
#include <iostream>
template <typename T, size_t Size>
class ConstexprVector {
private:
std::array<T, Size> data;
size_t current_size = 0;
public:
constexpr ConstexprVector() : current_size(0) {}
constexpr void push_back(const T& value) {
if (current_size < Size) {
data[current_size] = value;
current_size++;
}
}
constexpr size_t size() const {
return current_size;
}
constexpr const T& operator[](size_t index) const {
return data[index];
}
};
int main() {
constexpr ConstexprVector<int, 5> my_vector; // 声明一个容量为5的constexpr Vector
ConstexprVector<int, 5> another_vector; // 普通的vector
constexpr ConstexprVector<int, 5> initialized_vector = []() constexpr {
ConstexprVector<int, 5> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
return v;
}();
std::cout << "Size of initialized_vector: " << initialized_vector.size() << std::endl;
std::cout << "Element at index 1: " << initialized_vector[1] << std::endl;
return 0;
}
在这个例子中,我们自定义了一个ConstexprVector
类,它使用std::array
来存储数据,并提供了一个push_back
方法来添加元素。注意,这个push_back
方法是在编译期执行的,所以我们必须确保在编译期就知道要添加多少个元素。
编译期排序:
#include <array>
#include <iostream>
#include <algorithm>
template <typename T, size_t Size>
constexpr auto sort_array(std::array<T, Size> arr) {
std::sort(arr.begin(), arr.end());
return arr;
}
int main() {
constexpr std::array<int, 5> unsorted_array = {5, 2, 8, 1, 9};
constexpr auto sorted_array = sort_array(unsorted_array);
for (size_t i = 0; i < sorted_array.size(); ++i) {
std::cout << sorted_array[i] << " ";
}
std::cout << std::endl;
return 0;
}
这段代码尝试对一个std::array
进行排序,期望在编译期完成。但是,std::sort
在constexpr
上下文中使用通常是不允许的,因为它涉及到非constexpr
的操作。为了实现编译期排序,我们需要自己实现一个constexpr
的排序算法。
正确的constexpr排序(冒泡排序):
#include <array>
#include <iostream>
template <typename T, size_t Size>
constexpr std::array<T, Size> constexpr_sort(std::array<T, Size> arr) {
std::array<T, Size> sorted_arr = arr;
for (size_t i = 0; i < Size - 1; ++i) {
for (size_t j = 0; j < Size - i - 1; ++j) {
if (sorted_arr[j] > sorted_arr[j + 1]) {
// 交换元素
T temp = sorted_arr[j];
sorted_arr[j] = sorted_arr[j + 1];
sorted_arr[j + 1] = temp;
}
}
}
return sorted_arr;
}
int main() {
constexpr std::array<int, 5> unsorted_array = {5, 2, 8, 1, 9};
constexpr auto sorted_array = constexpr_sort(unsorted_array);
for (size_t i = 0; i < sorted_array.size(); ++i) {
std::cout << sorted_array[i] << " ";
}
std::cout << std::endl;
return 0;
}
这个例子使用冒泡排序算法,因为冒泡排序可以使用简单的if
语句和循环来实现,而这些都是constexpr
友好的。
实际应用场景
那么,这些编译期字符串和容器操作有什么实际用处呢?
- 配置解析: 在编译期解析配置文件,生成代码。
- 静态数据: 将一些静态数据存储在编译期容器中,避免运行时的初始化开销。
- 代码生成: 根据编译期的数据,生成不同的代码分支。
- 元编程: 实现一些复杂的模板元编程技巧,例如编译期计算数学公式、生成查找表等。
表格总结
特性 | std::string | std::vector |
---|---|---|
C++20 constexpr | 有限支持,避免动态内存分配。可以使用std::string_view 和std::array 替代。 |
有限支持,避免动态内存分配。可以使用std::array 或自定义静态数组类替代。 |
限制 | 动态内存分配,非constexpr 方法。 |
动态内存分配,非constexpr 方法。 |
替代方案 | std::string_view , std::array |
std::array , 自定义静态数组类 |
应用场景 | 编译期字符串处理,静态字符串存储。 | 编译期数据存储,静态数据初始化。 |
高级技巧:constexpr Lambda表达式
C++17引入了constexpr
lambda表达式,这使得在编译期执行一些复杂的操作变得更加容易。在C++20中,constexpr
lambda表达式的功能更加强大。
#include <iostream>
int main() {
constexpr auto add = [](int a, int b) constexpr { return a + b; };
constexpr int result = add(5, 3);
std::cout << "Result: " << result << std::endl;
return 0;
}
使用constexpr Lambda表达式初始化数组:
#include <array>
#include <iostream>
int main() {
constexpr std::array<int, 5> my_array = []() constexpr {
std::array<int, 5> arr{};
for (size_t i = 0; i < arr.size(); ++i) {
arr[i] = static_cast<int>(i * 2);
}
return arr;
}();
for (int val : my_array) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
注意事项:
- 编译时间: 编译期计算可能会增加编译时间。
- 代码可读性: 过度使用
constexpr
可能会降低代码的可读性。 - 平台兼容性: 不同的编译器对
constexpr
的支持程度可能不同。
总结:
C++20的constexpr
std::string
和std::vector
为我们打开了一扇新的大门,让我们可以在编译期进行更多的计算和操作。虽然目前还有一些限制,但随着C++的不断发展,相信这些限制会越来越少,我们可以用constexpr
做更多的事情。
记住,玩转constexpr
的关键在于避免动态内存分配,并尽可能使用constexpr
友好的数据结构和算法。希望今天的分享能帮助大家更好地理解和使用C++20的constexpr
特性。
各位,下次再见!