-
【C++中用ZMQ通讯】【操作步骤&问题现象】1、在本地笔记本和mdchost中,用终端指定动态库-lzmq编译带有ZMQ通讯模块的C++文件,来生成main.o文件,进而执行,都是成功的。见图一2、可是在mdc development studio中编译带有ZMQ通讯功能的MDC工程不能成功,显示编译失败。见图二【截图信息】图一图二 【日志信息】(可选,上传日志内容或者附件)
-
lower_bound,upper_bound和equal_range函数初识 lower_bound.(k) 返回一个迭代器,指向第一个值不小于k的元素upper_bound(k) 返回一个迭代器,指向第一个值大于k的元素equal_range(k) 返回一个迭代器pair,表示值等于k的元素的范围。若k不存在,pair两个成员均等于end()–尾迭代器 上面三个函数多用于容器中使用,但是对于普通的数组也是可以使用的,下面会讲到. 如果所查找值在容器中,lower_bound返回的迭代器将指向第一个具有给定值的元素,而upper_bound返回的迭代器指向最后一个匹配给定值的元素之后的位置。 如果元素不在容器中,则lower_bound和upper_bound会返回相等的迭代器----指向一个不影响排序的值插入位置 因此,用相同的值调用lower_bound和upper_bound会得到一个迭代器的范围,表示具有该关键字的元素范围。 当然,这两个操作返回的迭代器可能是容器的尾后迭代器。如果我们查找的元素具有容器中最大值,则此关键字的upper_bound返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound返回的也是尾后迭代器.
-
【功能模块】C++工程中ZMQ通讯库 的 安装 ,安装不上,请求协助【操作步骤&问题现象】1、各种 版本错误2、【截图信息】【日志信息】(可选,上传日志内容或者附件)
-
某软件对接了AWS S3,该系统C++开发;使用AWS S3 C++ SDK; 若该系统迁移到OBS需要做哪些改动? 有没有参考方案或者指导?
-
现象:开源2.9.1版本openvswitch编译不过,但是开源2.11.4版本可以编译通过,我们想在2.9.1 ovs编译,帮忙看看怎么解决呢?十分感谢编译环境,服务器 鲲鹏服务器操作系统:ARM centos内核:4.14.131openvswitch版本:2.9.1内核报错信息如下:make rpm-fedora-kmodld: unrecognized option '-Wl,-z,relro'ld: use the --help option for usage informationmake[2]: *** [/root/ovs/rpm/rpmbuild/BUILD/openvswitch-2.10.0/datapath/linux/reciprocal_div.o] Error 1make[2]: *** Waiting for unfinished jobs....
-
【功能模块】c/c++性能分析模块,运行出现运行返回错误提示 失败原因: exit status 1,其他部分的功能可以运行,只有这个功能【操作步骤&问题现象】1、按照https://support.huaweicloud.com/ug-spt-kunpengdevps/kunpengsys_06_0046.html中操作2、返回错误信息失败原因: exit status 1,见下图【截图信息】【日志信息】(可选,上传日志内容或者附件)
-
gdb调试代码显示 Missing separate debuginfos, use: dnf debuginfo-install libgcc-7.3.0-20190804.h24.eulerosv2r8.aarch64 libstdc++-7.3.0-20190804.h24.eulerosv2r8.aarch64执行dnf debuginfo-install 怎么解决?
-
目前用的鲲鹏服务器,同时我们在上面安装了IoTDB软件,java语言研发的软件。目前部署在上面,基本上程序半个小时会死一次。我们工程师和IoTDB的研发人员一起定位到了问题。最终确定是由于操作系统层面的一个包版本过低导致的,但是我们去升级版本,会报段错误,最终包和版本为: libstdc++.so.6.0.25, 我们自己升级的出错内容如截图,求助各位大佬,需要怎么去解决这个问题
-
截取子数组std::array并未提供输入一个指定区间来建立新容器的构造函数,但是借助上面的数组生成器,我们可以写个辅助函数来实现子数组生成操作(这里再次用上了lambda函数作为生成算法)。template<size_t N, typename T>constexpr auto SubArray(T&& t, size_t base) noexcept{ return GenerateArray<N>([base, t = forward<T>(t)](size_t i) { return t[base + i]; });} template<size_t N, typename T, size_t M>constexpr auto SubArray(const T (&t)[M], size_t base) noexcept{ return GenerateArray<N>([base, &t](size_t i) { return t[base + i]; });} int main(){ // 以std::initializer_list字面量为原始数据 constexpr auto x = SubArray<3>({1, 2, 3, 4, 5, 6}, 2); // 下标为2开始,取3个元素 static_assert(Equals(x, {3, 4, 5})); // 以std::array为原始数据 constexpr auto x1 = SubArray<2>(x, 1); // 下标为1开始,取2个元素 static_assert(Equals(x1, {4, 5})); // 以原生数组为原始数据 constexpr uint8_t a[] = {9, 8, 7, 6, 5}; constexpr auto y = SubArray<2>(a, 3); static_assert(Equals(y, {6, 5})); // 下标为3开始,取2个元素 // 以字符串为原始数据,注意生成的数组不会自动加上'\0' constexpr const char* str = "Hello world!"; constexpr auto z = SubArray<5>(str, 6); static_assert(Equals(z, {'w', 'o', 'r', 'l', 'd'})); // 下标为6开始,取5个元素 // 以std::vector为原始数据,非编译期计算 vector<int32_t> v{10, 11, 12, 13, 14}; size_t n = 2; auto d = SubArray<3>(v, n); // 运行时生成数组 assert(Equals(d, {12, 13, 14})); // 注意不能用static_assert,不是编译期常量 return 0;}使用SubArray时,模板参数N是要截取的子数组大小,入参t是任意能支持下标操作的类型,入参base是截取元素的起始位置。由于std::array的大小在编译期是确定的,因此N必须是编译期常量,但参数base可以是运行时变量。当所有入参都是编译期常量时,生成的子数组也是编译期常量。SubArray提供了两个版本,目的也是为了让std::initializer_list字面量可以作为参数传入。拼接多个数组采用类似的方式可以做多个数组的拼接,这里同样用了lambda作为生成函数。template<typename T>constexpr auto TotalLength(const T& arr) noexcept{ return size(arr);} template<typename P, typename... T>constexpr auto TotalLength(const P& p, const T&... arr) noexcept{ return size(p) + TotalLength(arr...);} template<typename T>constexpr auto PickElement(size_t i, const T& arr) noexcept{ return arr[i];} template<typename P, typename... T>constexpr auto PickElement(size_t i, const P& p, const T&... arr) noexcept{ if (i < size(p)) { return p[i]; } return PickElement(i - size(p), arr...);} template<typename... T>constexpr auto ConcatArrays(const T&... arr) noexcept{ return GenerateArray<TotalLength(arr...)>([&arr...](size_t i) { return PickElement(i, arr...); });} int main(){ constexpr int32_t a[] = {1, 2, 3}; // 原生数组 constexpr auto b = to_typed_array<int32_t>({4, 5, 6}); // std::array constexpr auto c = DeclareArray<int32_t, 7, 8>(); // std::array constexpr auto x = ConcatArrays(a, b, c); // 把3个数组拼接在一起 static_assert(Equals(x, {1, 2, 3, 4, 5, 6, 7, 8})); return 0;}和之前一样,ConcatArrays使用了模板参数来同时兼容原生数组和std::array,它甚至可以接受任何编译期确定长度的自定义类型参与拼接。ConcatArrays函数因为可变参数的语法限制,没有再对std::initializer_list字面量进行适配,这导致std::initializer_list字面量不能再直接作为参数:constexpr auto x = ConcatArrays(a, {4, 5, 6}); // 编译错误但是我们有办法规避这个问题:利用前面介绍过的工具把std::initializer_list先转成std::array就可以了:constexpr auto x = ConcatArrays(a, to_array({4, 5, 6})); // OK编译期拼接字符串std::array适合用来表示字符串么?回答这个问题前,我们先看看原生数组是否适合表示字符串:char str[] = "abc"; // str数组大小为4,包括结尾的'\0'上面是很常见的写法。由于数组名可退化为指针,str可用于各种需要字符串的场合,如传给cout打印输出。std::array作为对原生数组的替代,自然也适合用来表示字符串。有人可能会觉得std::array没法直接作为字符串类型使用,不太方便。但实际上只要调用data方法,std::array就会返回能作为字符串使用的指针:constexpr auto str = to_array("abc"); // to_array可以将字符串转换为std::arraystatic_assert(str.size() == 4);static_assert(Equals(str, "abc")); // Equals也可以接受字符串字面量cout << str.data(); // 打印字符串内容由于字符串字面量是char[]类型,因此前面所编写的工具函数,都可以将字符串作为输入参数。上面的Equals只是其中一个例子。那之前写的数组拼接函数ConcatArrays能用于拼接字符串么?能,但结果和我们想的有差异:constexpr auto str = ConcatArrays("abc", "def");static_assert(str.size() == 8); // 长度不是7?static_assert(Equals(str, {'a', 'b', 'c', '\0', 'd', 'e', 'f', '\0'}));因为每个字符串结尾都有'\0'结束符,用数组拼接方法把它们拼到一起时,中间的'\0'没有被去掉,导致结果字符串被切割为了多个C字符串。这个问题解决起来也很容易,只要在拼接数组时把所有数组的最后一个元素('\0')去掉,并且在返回数组的末尾加上'\0'就可以了。下面的代码实现了字符串拼接功能,非类型参数E是字符串的结束符,通常为'\0',但是也允许定制。我们甚至可以利用它来拼接结束符为其他值的对象,比如消息、报文等。// 最后一个字符,放入结束符template<auto E>constexpr auto PickChar(size_t i){ return E;} template<auto E, typename P, typename... T>constexpr auto PickChar(size_t i, const P& p, const T&... arr){ if (i < (size(p) - 1)) { if (p[i] == E) { // 结束符不允许出现在字符串中间 throw "terminator in the middle"; } return p[i]; } if (p[size(p) - 1] != E) { // 结束符必须是最后一个字符 throw "terminator not at end"; } return PickChar<E>(i - (size(p) - 1), arr...);} template<typename... T, auto E = '\0'>constexpr auto ConcatStrings(const T&... str){ return GenerateArray<TotalLength(str...) - sizeof...(T) + 1>([&str...](size_t i) { return PickChar<E>(i, str...); });} int main(){ constexpr char a[] = "I "; // 原生数组形式的字符串 constexpr auto b = to_array("love "); // std::array形式的字符串 constexpr auto str = ConcatStrings(a, b, "C++"); // 拼接 数组 + std::array + 字符串字面量 static_assert(Equals(str, "I love C++")); return 0;}这段代码中用了两个throw,这是为了校验输入的参数是否都为合法的字符串,即:字符串长度=容器长度-1。如果不符合该条件,会导致拼接结果的长度计算错误。当编译期的计算抛出异常时,只会出现编译错误,因此只要不在运行时调用ConcatStrings,这两个throw语句不会有更多影响。但因为这个校验的存在,强烈不建议在运行期调用ConcatStrings做拼接,何况运行期也没必要用这种方法——std::string的加法操作它不香么?有人会想:能否在编译期计算字符串的实际长度,而不是用容器的长度呢?这个方法看似可行,定义一个编译期计算字符串长度的函数确实很容易:template<typename T, auto E = '\0'>constexpr size_t StrLen(const T& str) noexcept{ size_t i = 0; while (str[i] != E) { ++i; } return i;} constexpr const char* g_str = "abc"; int main(){ // 利用StrLen把一个字符串按实际长度转成std::array constexpr auto str = SubArray<StrLen(g_str) + 1>(g_str, 0); static_assert(Equals(str, "abc")); return 0;}但是,一旦你试图把StrLen放到ConcatStrings的内部去声明数组长度,就会产生问题:C++的constexpr机制要求只有在能看到输入参数的constexpr属性的地方,才允许StrLen的返回结果确定为constexpr。而在函数内部时,看到的参数类型并不是constexpr。当然我们可以变通一下,做出一些有趣的工具,比如使用万恶的宏:// 把一个字符串按实际长度转成std::array#define StrToArray(x) SubArray<StrLen(x) + 1>(x, 0) constexpr const char* g_str = "abc"; int main(){ // 使用宏,可以让constexpr指针类型也参与编译期字符串的拼接 constexpr auto str = ConcatStrings(StrToArray(g_str), "def"); static_assert(Equals(str, "abcdef")); return 0;}使用宏以后,ConcatStrings连编译期不确定大小的指针类型都可以间接作为输入了[3]。如果你狠得下心使用变参宏,甚至可以定义出按实际字符串长度计算结果数组长度的更通用拼接函数。但我严重怀疑这种需求的必要性——毕竟我们只是做编译期的拼接,而编译期的字符串不应该会有结束符位置不在末尾的场景。看到这里的人,或多或少该佩服一下std::array的强大了。上面这些编译期操作,用原生数组很难完成吧?展望C++20——打破更多的枷锁我在文章中说了多少次“至少在C++20之前如此”?不记得了,但是能确定的是:C++20会带来很多美好的东西:std::array会有constexpr版本的比较运算符;函数可以用consteval限定只在编译期调用;模板非类型参数允许更多的类型;STL容器对象可以作为constexpr常量……所有这一切,都只是C++20的minor更新而已,在绝大多数的特性介绍中,它们连提都不会被提到!可想而知,用上C++20以后,编程会发生多大的变化。那时我们再来找找更多有趣的用法尾注[1] to_array定义了两个版本,分别以左值引用和右值引用作为参数类型。按照C++11的最优实践,这样的函数本应该只定义一个版本并且使用完美转发。但是to_array的场景如果用万能引用会带来一个问题:C++禁止std::initializer_list字面量{...}被推导为模板类型参数,完美转发方案会导致std::initializer_list字面量不能作为to_array的入参。在后面内容中我们会看到多次这个限制所带来的影响。[2] C++20加入了consteval修饰符,可以指定函数只允许在编译期调用。[3] 需要注意的是:constexpr用于修饰指针时,表示的是指针本身为常量(而不是其指向的对象)。和const不同,constexpr并不允许放在类型声明表达式的中间。因此如果要在编译期计算一个constexpr指针指向的字符串长度,这个字符串必须位于静态数据区里,不能位于栈或者堆上(否则其地址无法在编译期确定)。
-
编译期生成数组C++11中新增的constexpr修饰符可以在编译期完成很多计算工作。但是一般constexpr函数只能返回单个值,一旦你想用它返回一串对象的集合,就会遇到麻烦:STL容器都有动态内存申请功能,不能作为编译期常量(至少在C++20之前如此);而原生数组作为返回值会退化为指针,导致返回悬空的指针。即使是返回数组的引用也是不行的,会产生悬空的引用。constexpr int* Func() noexcept{ int a[] = {1, 2, 3, 4}; return a; // 严重错误!返回局部对象的地址}直到std::array的出现,这个问题才得到较好解决。std::array既可以作为编译期常量,又可以作为函数返回值。于是,它成为了编译期返回集合数据的首选。在上面to_array等工具函数的实现中,我们已经见过了编译期返回数组是怎么做的。这里我们再大胆一点,写一个编译期冒泡排序:template<typename T, size_t N>constexpr std::array<T, N> Sort(const std::array<T, N>& numbers) noexcept{ std::array<T, N> sorted(numbers); for (int i = 0; i < N; ++i) { for (int j = N - 1; j > i; --j) { if (sorted[j] < sorted[j - 1]) { T t = sorted[j]; sorted[j] = sorted[j - 1]; sorted[j - 1] = t; } } } return sorted;} int main(){ constexpr std::array<int, 4> before{4, 2, 3, 1}; constexpr std::array<int, 4> after = Sort(before); static_assert(after[0] == 1); static_assert(after[1] == 2); static_assert(after[2] == 3); static_assert(after[3] == 4); return 0;}因为整个排序算法都是在编译期完成,所以我们没有必要太关注冒泡排序的效率问题。当然,只要你愿意,完全可以写出一个编译期快速排序——毕竟constexpr函数也可以在运行期使用,不好说会不会有哪个憨憨在运行时调用它。 在编写constexpr函数时,有两点需要注意: 1. constexpr函数中不能调用非constexpr函数。因此在交换元素时不能用std::swap,排序也不能直接调用std::sort。 2. 传入的数组是constexpr的,因此参数类型必须加上const,也不能对数据进行就地排序,必须返回一个新的数组。 虽然限制很多,但编译期算法的好处也是巨大的:如果运算中有数组越界等未定义行为,编译将会失败。相比起运行时的测试,编译期测试constexpr函数能有效的提前拦截问题。而且只要编译通过就意味着测试通过,比起额外跑白盒测试用例方便多了。 上面的一大串static_assert语句让人看了不舒服。这么写的原因是std::array的operator==函数并非constexpr(至少在C++20前如此)。但是我们也可以自己定义一个模板函数用于判断两个数组是否相等: template<typename T, typename U, size_t M, size_t N> constexpr bool EqualsImpl(const T& lhs, const U& rhs) { static_assert(M == N); for (size_t i = 0; i < M; ++i) { if (lhs[i] != rhs[i]) { return false; } } return true; } template<typename T, typename U> constexpr bool Equals(const T& lhs, const U& rhs) { return EqualsImpl<T, U, size(lhs), size(rhs)>(lhs, rhs); } template<typename T, typename U, size_t N> constexpr bool Equals(const T& lhs, const U (&rhs)[N]) { return EqualsImpl<T, const U (&)[N], size(lhs), N>(lhs, rhs); } int main() { constexpr std::array<int, 4> before{4, 2, 3, 1}; constexpr std::array<int, 4> after = Sort(before); static_assert(Equals(after, {1, 2, 3, 4})); // 比较std::array和原生数组 static_assert(!Equals(before, after)); // 比较两个std::array return 0; } 我们定义的Equals比std::array的比较运算符更强大,甚至可以在std::array和原生数组之间进行比较。 对于Equals有两点需要说明: 1. std::size是C++17提供的工具函数,对各种容器和数组都能返回其大小。当然,这里的Equals只会允许编译期确定大小的容器传入,否则触发编译失败。 2. Equals定义了两个版本,这是被C++的一个限制所逼的迫不得已:C++禁止{...}这种std::initializer_list字面量被推导为模板参数类型,因此我们必须提供一个版本声明参数类型为数组,以便{1, 2, 3, 4}这种表达式能作为参数传进去。 编译期排序是一个启发性的尝试,我们可以用类似的方法生成其他的编译期集合常量,比如指定长度的自然数序列: template<typename T, size_t N> constexpr auto NaturalNumbers() noexcept { array<T, N> arr{0}; // 显式初始化不能省 for (size_t i = 0; i < N; ++i) { arr[i] = i + 1; } return arr; } int main() { constexpr auto arr = NaturalNumbers<uint32_t, 5>(); static_assert(Equals(arr, {1, 2, 3, 4, 5})); return 0; } 这段代码的编译运行都没有问题,但它并不是推荐的做法。原因是在NaturalNumbers函数中,先定义了一个内容全0的局部数组,然后再挨个修改它的值,这样没有直接返回指定值的数组效率高。有人会想能不能把arr的初始化给去掉,但这样会导致编译错误——constexpr函数中不允许定义没有初始化的局部变量。 可能有人觉得这些计算都是编译期完成的,对运行效率没影响——但是不要忘了constexpr函数也可以在运行时调用。更好的做法可以参见前面to_array函数的实现,让数组的初始化一气呵成,省掉挨个赋值的步骤。 我们用这个新思路,写一个通用的数组生成器,它可以接受一个函数对象作为参数,通过调用这个函数对象来生成数组每个元素的值。下面的代码还演示了下如何用这个生成器在编译期生成奇数序列和斐波那契数列。 template<typename T> constexpr T OddNumber(size_t i) noexcept { return i * 2 + 1; } template<typename T> constexpr T Fibonacci(size_t i) noexcept { if (i <= 1) { return 1; } return Fibonacci<T>(i - 1) + Fibonacci<T>(i - 2); } template<typename T, size_t N, typename F, size_t... I> constexpr array<std::remove_cv_t<T>, N> GenerateArrayImpl(F f, std::index_sequence<I...>) noexcept { return { {f(I)...} }; } template<size_t N, typename F, typename T = invoke_result_t<F, size_t>> constexpr array<T, N> GenerateArray(F f) noexcept { return GenerateArrayImpl<T, N>(f, std::make_index_sequence<N>{}); } int main() { constexpr auto oddNumbers = GenerateArray<5>(OddNumber<uint8_t>); static_assert(Equals(oddNumbers, {1, 3, 5, 7, 9})); constexpr auto fiboNumbers = GenerateArray<5>(Fibonacci<uint32_t>); static_assert(Equals(fiboNumbers, {1, 1, 2, 3, 5})); // 甚至可以传入lambda来定制要生成的数字序列(限定C++17) constexpr auto specified = GenerateArray<3>([](size_t i) { return i + 10; }); static_assert(Equals(specified, {10, 11, 12})); return 0; } 最后那个传入lambda来定制数组的做法存在一个疑问:lambda是constexpr函数吗?答案为:可以是,但需要C++17支持。 GenerateArray这个数组生成器将会在后面发挥重大作用,继续往下看。
-
摘要:在这篇文章里,将从各个角度介绍下std::array的用法,希望能带来一些启发。td::array是在C++11标准中增加的STL容器,它的设计目的是提供与原生数组类似的功能与性能。也正因此,使得std::array有很多与其他容器不同的特殊之处,比如:std::array的元素是直接存放在实例内部,而不是在堆上分配空间;std::array的大小必须在编译期确定;std::array的构造函数、析构函数和赋值操作符都是编译器隐式声明的……这让很多用惯了std::vector这类容器的程序员不习惯,觉得std::array不好用。但实际上,std::array的威力很可能被低估了。在这篇文章里,我会从各个角度介绍下std::array的用法,希望能带来一些启发。本文的代码都在C++17环境下编译运行。当前主流的g++版本已经能支持C++17标准,但是很多版本(如gcc 7.3)的C++17特性不是默认打开的,需要手工添加编译选项-std=c++17。自动推导数组大小很多项目中都会有类似这样的全局数组作为配置参数:uint32_t g_cfgPara[] = {1, 2, 5, 6, 7, 9, 3, 4};当程序员想要使用std::array替换原生数组时,麻烦来了:array<uint32_t, 8> g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4}; // 注意模板参数“8”程序员不得不手工写出数组的大小,因为它是std::array的模板参数之一。如果这个数组很长,或者经常增删成员,对数组大小的维护工作恐怕不是那么愉快的。有人要抱怨了:std::array的声明用起来还没有原生数组方便,选它干啥?但是,这个抱怨只该限于C++17之前,C++17带来了类模板参数推导特性,你不再需要手工指定类模板的参数:array g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4}; // 数组大小与成员类型自动推导看起来很美好,但很快就会有人发现不对头:数组元素的类型是什么?还是std::uint32_t吗?有人开始尝试只提供元素类型参数,让编译器自动推导长度,遗憾的是,它不会奏效。array<uint32_t> g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4}; // 编译错误好吧,暂时看起来std::array是不能像原生数组那样声明。下面我们来解决这个问题。用函数返回std::array问题的解决思路是用函数模板来替代类模板——因为C++允许函数模板的部分参数自动推导——我们可以联想到std::make_pair、std::make_tuple这类辅助函数。巧的是,C++标准真的在TS v2试验版本中推出过std::make_array,然而因为类模板参数推导的问世,这个工具函数后来被删掉了。但显然,用户的需求还是存在的。于是在C++20中,又新增了一个辅助函数std::to_array。别被C++20给吓到了,这个函数的代码其实很简单,我们可以把它拿过来定义在自己的C++17代码中[1]。template<typename R, typename P, size_t N, size_t... I>constexpr array<R, N> to_array_impl(P (&a)[N], std::index_sequence<I...>) noexcept{ return { {a[I]...} };} template<typename T, size_t N>constexpr auto to_array(T (&a)[N]) noexcept{ return to_array_impl<std::remove_cv_t<T>, T, N>(a, std::make_index_sequence<N>{});} template<typename R, typename P, size_t N, size_t... I>constexpr array<R, N> to_array_impl(P (&&a)[N], std::index_sequence<I...>) noexcept{ return { {move(a[I])...} };} template<typename T, size_t N>constexpr auto to_array(T (&&a)[N]) noexcept{ return to_array_impl<std::remove_cv_t<T>, T, N>(move(a), std::make_index_sequence<N>{});}细心的朋友会注意到,上面这个定义与C++20的推荐实现有所差异,这是有目的的。稍后我会解释这么干的原因。现在让我们尝试下用新方法解决老问题:auto g_cfgPara = to_array<int>({1, 2, 5, 6, 7, 9, 3, 4}); // 类型不是uint32_t?不对啊,为什么元素类型不是原来的std::uint32_t?这是因为模板参数推导对std::initializer_list的元素拒绝隐式转换,如果你把to_array的模板参数从int改为uint32_t,会得到如下编译错误:D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: error: no matching function for call to 'to_array<uint32_t>(<brace-enclosed initializer list>)' auto g_cfgPara = to_array<uint32_t>({1, 2, 5, 6, 7, 9, 3, 4});D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:34:16: note: candidate: 'template<class T, long long unsigned int N> constexpr auto to_array(T (&)[N])' constexpr auto to_array(T (&a)[N]) noexcept ^~~~~~~~D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:34:16: note: template argument deduction/substitution failed:D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: note: mismatched types 'unsigned int' and 'int' auto g_cfgPara = to_array<uint32_t>({1, 2, 5, 6, 7, 9, 3, 4});D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:46:16: note: candidate: 'template<class T, long long unsigned int N> constexpr auto to_array(T (&&)[N])' constexpr auto to_array(T (&&a)[N]) noexcept ^~~~~~~~D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:46:16: note: template argument deduction/substitution failed:D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: note: mismatched types 'unsigned int' and 'int'Hoho,有点惨是不,绕了一圈回到原点,还是不能强制指定类型。这个时候,之前针对std::array做的修改派上用场了:我给to_array_impl增加了一个模板参数,让输入数组的元素和返回std::array的元素用不同的类型参数表示,这样就给类型转换带来了可能。为了实现转换到指定的类型,我们还需要添加两个工具函数:template<typename R, typename P, size_t N>constexpr auto to_typed_array(P (&a)[N]) noexcept{ return to_array_impl<R, P, N>(a, std::make_index_sequence<N>{});} template<typename R, typename P, size_t N>constexpr auto to_typed_array(P (&&a)[N]) noexcept{ return to_array_impl<R, P, N>(move(a), std::make_index_sequence<N>{});}这两个函数和to_array的区别是:它带有3个模板参数:第一个是要返回的std::array的元素类型,后两个和to_array一样。这样我们就可以通过指定第一个参数来实现定制std::array元素类型了。auto g_cfgPara = to_typed_array<uint32_t>({1, 2, 5, 6, 7, 9, 3, 4}); // 自动把元素转换成uint32_t这段代码可以编译通过和运行,但是却有类型转换的编译告警。当然,如果你胆子够大,可以在to_array_impl函数中放一个static_cast来消除告警。但是编译告警提示了我们一个不能忽视的问题:如果万一输入的数值溢出了怎么办?auto g_a = to_typed_array<uint8_t>({256, -1}); // 数字超出uint8_t范围编译器还是一样的会让你编译通过和运行,g_a中的两个元素的值将分别为0和255。如果你不明白为什么这两个值和入参不一样,你该复习下整型溢出与回绕的知识了。显然,这个方案还不完美。但我们可以继续改进。编译期字面量数值合法性校验首先能想到的做法是在to_array_impl函数中放入一个if判断之类的语句,对于超出目标数值范围的输入抛出异常或者做其他处理。这当然可行,但要注意的是这些工具函数是可以在运行期调用的,对于这种常用的基础函数来说,性能至关重要。一旦在里面加入了错误判断,意味着运行时的每一次调用性能都会下降。理想的设计是:只有在编译期生成的数组才进行校验,并且报编译错误。但运行时调用函数时不要加入任何校验。可惜的是,至少在C++20之前,没有办法指定函数只允许在编译期执行[2]。那有没有其他手段呢?熟悉C++的人知道:C++的编译期处理大多可以用模板的trick来完成——因为模板参数一定是编译期常量。因此我们可以用模板参数来完成编译期处理——只要把数组元素全部作为模板的非类型参数就可以了。当然,这里有个问题:模板的非类型参数的类型怎么确定?正好C++17提供了auto模板参数的功能,可以派上用场:template<typename T>constexpr void CheckIntRanges() noexcept {} // 用于终结递归template<typename T, auto M, auto... N>constexpr void CheckIntRanges() noexcept{// 防止无符号与有符号比较static_assert(!((std::numeric_limits<T>::min() >= 0) && (M < 0))); // 范围校验 static_assert((M >= std::numeric_limits<T>::min()) &&(M <= std::numeric_limits<T>::max()));CheckIntRanges<T, N...>();}template<typename T, auto... N>constexpr auto DeclareArray() noexcept{CheckIntRanges<T, N...>();array<T, sizeof...(N)> a{{static_cast<T>(N)...}};return a;};注意这个函数中,所有的校验都通过static_assert完成。这就保证了校验一定只会发生在编译期,不会带来任何运行时开销。DeclareArray的使用方法如下:constexpr auto a1 = DeclareArray<uint8_t, 1, 2, 3, 4, 255>(); // 声明一个std::array<uint8_t, 5>,元素分别为1, 2, 3, 4, 255static_assert(a1.size() == 5);static_assert(a1[3] == 4);auto a2 = DeclareArray<uint8_t, 1, 2, 3, -1>(); // 编译错误,-1超出uint8_t范围auto a3 = DeclareArray<uint16_t, 1, 2, 3, 65536>(); // 编译错误,65536超出uint16_t范围这里有一个误区需要说明:有些人可能会把DeclareArray声明成这样:template<typename T, T... N> // 注意N的类型为Tconstexpr auto DeclareArray() noexcept这么做的话,会发现对数值的校验总是能通过——因为模板参数在进入校验之前就已经被转换为T类型了。如果你的编译器不支持C++17的auto模板参数,那么可以通过使用std::uint64_t、std::int64_t这些“最大”的类型来间接达到目的。另一点要说明的是,C++对于非类型模板参数的允许类型存在限制,DeclareArray的方法只能用于数组元素为基本类型的场景(至少在C++20以前如此)。但是这也足够了。如果数组的元素是自定义类型,就可以通过自定义的构造函数等方法来控制类型转换。如果你看到这里觉得有点意思了,那就对了,后面还有更过瘾的。
-
首先说一下实验沙箱的优点:可以使用云服务器上的环境练习,改变了传统本地部署,减少工作量,提高了效率。本实验目标: 1、掌握代码扫描工具的使用 2、掌握crcutil迁移组件的使用 3、熟悉编译安装的过程实验步骤: 第一步:预置实验环境 第二步:使用上图里的账户名、用户名、密码复制粘贴到相应位置,点击“登录”实验账号 第三步:使用“Xfce终端”登陆云服务器,注意:登录命令:LANG=en_us.UTF-8 ssh root@124.71.129.5 第四步:代码扫描工具的使用 安装和使用代码扫描工具 第五步:crcutil组件迁移 编译脚本和源码的修改、源码修改、源码编译安装、组件测试 注意:5.2.2步骤(如下图)里,56行虽然已经有#endif,担仍需要在55行处添加#endif语句,不然后面会报错。 总结: 1、通过实验可以完全模拟真实环境,可以熟悉实验步骤,可以查找操作错误,避免真实项目中出现问题 2、linux基本操作:cd 、make、vim的使用编辑 3、文件配置需要细心、小心
-
摘要:C++调用Go方法时,字符串参数的内存管理需要由Go侧进行深度值拷贝。现象在一个APP技术项目中,子进程按请求加载Go的ServiceModule,将需要拉起的ServiceModule信息传递给Go的Loader,存在C++调用Go方法,传递字符串的场景。方案验证时,发现有奇怪的将std::string对象的内容传递给Go方法后,在Go方法协程中取到的值与预期不一致。经过一段时间的分析和验证,终于理解问题产生的原因并给出解决方案,现分享如下。背景知识Go有自己的内存回收GC机制,通过make等申请的内存不需要手动释放。C++中为std::string变量赋值新字符串后,.c_str()和.size()的结果会联动变化,尤其是.c_str()指向的地址也有可能变化。go build -buildmode=c-shared .生成的.h头文件中定义了C++中Go的变量类型的定义映射关系,比如GoString、GoInt等。其中GoString实际是一个结构体,包含一个字符指针和一个字符长度。原理及解释通过代码示例方式解释具体现象及原因,详见注释C++侧代码://// Created by w00526151 on 2020/11/5.// #include <string>#include <iostream>#include <unistd.h>#include "libgoloader.h" /*** 构造GoString结构体对象* @param p* @param n* @return*/GoString buildGoString(const char* p, size_t n){ //typedef struct { const char *p; ptrdiff_t n; } _GoString_; //typedef _GoString_ GoString; return {p, static_cast<ptrdiff_t>(n)};} int main(){ std::cout<<"test send string to go in C++"<<std::endl; std::string tmpStr = "/tmp/udsgateway-netconftemplateservice"; printf("in C++ tmpStr: %p, tmpStr: %s, tmpStr.size:%lu \r\n", tmpStr.c_str(), tmpStr.c_str(), tmpStr.size()); { //通过new新申请一段内存做字符串拷贝 char *newStrPtr = NULL; int newStrSize = tmpStr.size(); newStrPtr = new char[newStrSize]; tmpStr.copy(newStrPtr, newStrSize, 0); //调用Go方法,第一个参数直接传std::string的c_str指针和大小,第二个参数传在C++中单独申请的内存并拷贝的字符串指针,第三个参数和第一个一样,但是在go代码中做内存拷贝保存。 //调用Go方法后,通过赋值修改std::string的值内容,等待Go中新起的线程10s后再将三个参数值打印出来。 LoadModule(buildGoString(tmpStr.c_str(), tmpStr.size()), buildGoString(newStrPtr, newStrSize), buildGoString(tmpStr.c_str(),tmpStr.size())); //修改tmpStr的值,tmpStr.c_str()得到的指针指向内容会变化,tmpStr.size()的值也会变化,Go中第一个参数也会受到影响,前几位会变成新字符串内容。 //由于在Go中int是值拷贝,所以在Go中,第一个参数的长度没有变化,因此实际在Go中已经出现内存越界访问,可能产生Coredump。 tmpStr = "new string"; printf("in C++ change tmpStr and delete newStrPtr, new tmpStr: %p, tmpStr: %s, tmpStr.size:%lu \r\n", tmpStr.c_str(), tmpStr.c_str(), tmpStr.size()); //释放新申请的newStrPtr指针,Go中对应第二个string变量内存也会受到影响,产生乱码。 // 实际在Go中,已经在访问一段在C++中已经释放的内存,属于野指针访问,可能产生Coredump。 delete newStrPtr; } pause();}Go侧代码: package main import "C" import ( "fmt" "time" ) func printInGo(p0 string, p1 string, p2 string){ time.Sleep(10 * time.Second) fmt.Printf("in go function, p0:%s size %d, p1:%s size %d, p2:%s size %d", p0, len(p0), p1, len(p1), p2, len(p2)) } //export LoadModule func LoadModule(name string, version string, location string) int { //通过make的方式,新构建一段内存来存放从C++处传入的字符串,深度拷贝防止C++中修改影响Go tmp3rdParam := make([]byte, len(location)) copy(tmp3rdParam, location) new3rdParam := string(tmp3rdParam) fmt.Println("in go loadModule,first param is",name,"second param is",version, "third param is", new3rdParam) go printInGo(name, version, new3rdParam); return 0 } Go侧代码通过-buildmode=c-shared的方式生成libgoloader.so及libgoloader.h供C++编译运行使用 go build -o libgoloader.so -buildmode=c-shared . 程序执行结果: test send string to go in C++ in C++ tmpStr: 0x7fffe1fb93f0, tmpStr: /tmp/udsgateway-netconftemplateservice, tmpStr.size:38 # 将C++的指针传给Go,一开始打印都是OK的 in go loadModule,first param is /tmp/udsgateway-netconftemplateservice second param is /tmp/udsgateway-netconftemplateservice third param is /tmp/udsgateway-netconftemplateservice # 在C++中,将指针指向的内容修改,或者删掉指针 in C++ change tmpStr and delete newStrPtr, new tmpStr: 0x7fffe1fb93f0, tmpStr: new string, tmpStr.size:10 # 在Go中,参数1、参数2对应的Go string变量都受到了影响,参数3由于做了深度拷贝,没有受到影响。 in go function, p0:new string eway-netconftemplateservice size 38, p1: p netconftemplateservice size 38, p2:/tmp/udsgateway-netconftemplateservice size 38 结论 结论:C++调用Go方法时,字符串参数的内存管理需要由Go侧进行深度值拷贝。即参数三的处理方式 原因:传入的字符串GoString,实际是一个结构体,第一个成员p是一个char*指针,第二个成员n是一个int长度。 在C++代码中,任何对成员p的char*指针的操作,都将直接影响到Go中的string对象的值。 只有通过单独的内存空间开辟,进行独立内存管理,才可以避免C++中的指针操作对Go的影响。 ps:不在C++中进行内存申请释放的原因是C++无法感知Go中何时才能真的已经没有对象引用,无法找到合适的时间点进行内存释放。
-
摘要:近期踩到了一些比较隐晦的C++的坑,可把我们团队给坑惨了~~近期我们团队进行版本质量加固时,踩到了一些比较隐晦的C++的坑,特总结分享在此,供大家参考。1. string的字符串拼接,导致coredump该问题的核心点在于第9行,竟然是可以编译通过,其原因是x+"-",会被转成char*,然后与to_string叠加导致BUG。2. map的迭代器删除map要删除一个元素,通常通过erase()函数来完成,但是要注意,如果我们传入了一个iterator作为erase的参数来删除当前迭代器所指向的元素,删除完成后iterator会失效,产生未定义行为。正确的使用方法应该是接收erase()的返回值,让iterator指向被删除元素的下一个元素或者end()。for ( auto iter = m.begin(); iter != m.end(); iter++) { if (...) iter = m.erase(iter); } 但是上述代码仍然有错误,因为如果触发了删除,那么iter再下一轮循环时会指向下下个元素,所以正确的写法应该是: for ( auto iter = m.begin(); iter != m.end();) { if (...) { iter = m.erase(iter); continue ; } else { iter++; } } 3. stringstream的性能问题stringstream的清空是clear之后,置空。stringstream在任何情况下都比snprintf慢。memset是个很慢的函数,宁愿新创建对象。上述测试结果是单线程,改成多线程,同样成立。str += “a”, 比 str =str+ “a” 效率高很多,后者会创建新对象。4. 智能指针(shared_ptr)使用注意4.1尽量使用make_shared初始化提高性能std::shared_ptr<Widget> spw(newWidget);需要分配两次内存。每个std::shared_ptr都指向一个控制块,控制块包含被指向对象的引用计数以及其他东西。这个控制块的内存是在std::shared_ptr的构造函数中分配的。因此直接使用new,需要一块内存分配给Widget,还要一块内存分配给控制块autospw = std::make_shared<Widget>();一次分配就足够了。这是因为std::make_shared申请一个单独的内存块来同时存放Widget对象和控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。异常安全processWidget(std::shared_ptr<Widget>( new Widget), //潜在的资源泄露 computePriority()); 上述代码存在内存泄漏的风险,上述代码执行分为3个步骤:1. new Widget2. shared_ptr构造3. computePriority 编译器不需要必须产生这样顺序的代码,但“new Widget”必须在std::shared_ptr的构造函数被调用前执行。如果编译器产生的顺序代码如下:1. new Widget2. 执行computePriority。3. 执行std::shared_ptr的构造函数。 如果执行步骤2:computePriority的时候程序出现异常,则在第一步动态分配的Widget就会泄露了,因为它永远不会被存放到在第三步才开始管理它的shared_ptr中4.2 父类之类智能指针转换C++中是允许裸指针,因此裸指针之间转换方法同C语言指针强转,智能指针转换不能通过上述方法进行强转,必须通过库提供转换函数进行转换。 C++11的方法是:std::dynamic_pointer_cast;boost中的方法是:boost::dynamic_pointer_cast#include <memory>#include <boost/shared_ptr.hpp>#include <boost/make_shared.hpp>#include <iostream>class Base { public : Base(){} virtual ~Base() {}};class D : public Base { public : D(){} virtual ~D() {}};int main(){ //方式一:先初始化子类智能指针,然后调用dynamic_pointer_cast转换成基类智能指针对象 std::shared_ptr<D> d1 = std::make_shared<D>(); std::shared_ptr<Base> b1 = std::dynamic_pointer_cast<Base>(d1); //方式二:先new子类D的指针,然后调用shared_ptr的构造函数初始化基类智能指针 std::shared_ptr<Base> b2 = shared_ptr<Base>( new D()); return 0;} 结论方式一和方式二均能够实现基类智能指针指向子类,但建议采用方式1,通过std::make_shared的方式构造智能指针,然后进行转换;5. map的安全查找办法即map[key]这种写法,就是会创建元素(且不一定初始化),因此在业务逻辑是希望查找的时候,就老老实实用find,不然会有脏数据写入。6. string 的指针构造std::string 的构造方式,除了与其它顺序容器相近的方式之外,提供了三种额外的构造方式:string s(cp, n): s 是cp指向的数组中前n个字符的拷贝,该数组至少应该包含n个字符string s(s2, pos2):s 是string s2从下标pos2开始的字符的拷贝,若pos2>s2.size(),构造函数的行为未定义string s(s2, pos2, len2):s 是string s2从下标pos2开始len2个字符的拷贝,若pos2>s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝s2.size()-pos2个字符 std::string 未提供 string(cp, pos2, len2) 这种构造方式,如果代码中使用了该方式,最终会将 cp 指向的数组构造成一个string,然后调用string(s2, pos2, len2)这种构造方式。不提供string(cp, pos2, len2)这种构造方式原因在于:使用这种方式构造容易出现问题,cp是一个指针,通常使用时,能获得其数组长度并检查传入参数;若传入两个参数,容易出现越界。7. 变量初始化变量初始化总是没错的,不管后面是否会修改该值。尤其是int等内建的类型,在类或struct中及容易忽略初始化,使变量成为随机值,产生不可预知的错误。变量请初始化!变量请初始化!!变量请初始化!!!
-
近期我们团队进行版本质量加固时,踩到了一些比较隐晦的C++的坑,特总结分享在此,供大家参考。1. string的字符串拼接,导致coredump。`该问题的核心点在于第9行,竟然是可以编译通过,其原因是x+"-",会被转成char*,然后与to_string叠加导致BUG。2. map的迭代器删除map要删除一个元素,通常通过erase()函数来完成,但是要注意,如果我们传入了一个iterator作为erase的参数来删除当前迭代器所指向的元素,删除完成后iterator会失效,产生未定义行为。正确的使用方法应该是接收erase()的返回值,让iterator指向被删除元素的下一个元素或者end()。 for (auto iter = m.begin(); iter != m.end(); iter++) { if (...) iter = m.erase(iter); }但是上述代码仍然有错误,因为如果触发了删除,那么iter再下一轮循环时会指向下下个元素,所以正确的写法应该是: for (auto iter = m.begin(); iter != m.end();) { if (...) { iter = m.erase(iter); continue; } else { iter++; } }3. stringstream的性能问题stringstream的清空是clear之后,置空。stringstream在任何情况下都比snprintf慢。memset是个很慢的函数,宁愿新创建对象。上述测试结果是单线程,改成多线程,同样成立。str += “a”, 比 str =str+ “a” 效率高很多,后者会创建新对象。4. 智能指针(shared_ptr)使用注意1.尽量使用make_shared初始化提高性能std::shared_ptr<Widget> spw(new Widget);需要分配两次内存。每个std::shared_ptr都指向一个控制块,控制块包含被指向对象的引用计数以及其他东西。这个控制块的内存是在std::shared_ptr的构造函数中分配的。因此直接使用new,需要一块内存分配给Widget,还要一块内存分配给控制块auto spw = std::make_shared<Widget>();一次分配就足够了。这是因为std::make_shared申请一个单独的内存块来同时存放Widget对象和控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。异常安全processWidget(std::shared_ptr<Widget>(new Widget), //潜在的资源泄露 computePriority());上述代码存在内存泄漏的风险,上述代码执行分为3个步骤:1. new Widget 2. shared_ptr构造 3. computePriority编译器不需要必须产生这样顺序的代码,但“new Widget”必须在std::shared_ptr的构造函数被调用前执行。如果编译器产生的顺序代码如下:1. new Widget 2. 执行computePriority。 3. 执行std::shared_ptr的构造函数。如果执行步骤2:computePriority的时候程序出现异常,则在第一步动态分配的Widget就会泄露了,因为它永远不会被存放到在第三步才开始管理它的shared_ptr中2. 父类之类智能指针转换C++中是允许裸指针,因此裸指针之间转换方法同C语言指针强转,智能指针转换不能通过上述方法进行强转,必须通过库提供转换函数进行转换。 C++11的方法是:std::dynamic_pointer_cast;boost中的方法是:boost::dynamic_pointer_cast#include <memory> #include <boost/shared_ptr.hpp> #include <boost/make_shared.hpp> #include <iostream> class Base { public: Base(){} virtual ~Base() {} }; class D : public Base { public: D(){} virtual ~D() {} }; int main() { //方式一:先初始化子类智能指针,然后调用dynamic_pointer_cast转换成基类智能指针对象 std::shared_ptr<D> d1 = std::make_shared<D>(); std::shared_ptr<Base> b1 = std::dynamic_pointer_cast<Base>(d1); //方式二:先new子类D的指针,然后调用shared_ptr的构造函数初始化基类智能指针 std::shared_ptr<Base> b2 = shared_ptr<Base>(new D()); return 0; }结论方式一和方式二均能够实现基类智能指针指向子类,但建议采用方式1,通过std::make_shared的方式构造智能指针,然后进行转换;5. map的安全查找办法即map[key]这种写法,就是会创建元素(且不一定初始化),因此在业务逻辑是希望查找的时候,就老老实实用find,不然会有脏数据写入。6. string 的指针构造std::string 的构造方式,除了与其它顺序容器相近的方式之外,提供了三种额外的构造方式:string s(cp, n): s 是cp指向的数组中前n个字符的拷贝,该数组至少应该包含n个字符 string s(s2, pos2):s 是string s2从下标pos2开始的字符的拷贝,若pos2>s2.size(),构造函数的行为未定义 string s(s2, pos2, len2):s 是string s2从下标pos2开始len2个字符的拷贝,若pos2>s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝s2.size()-pos2个字符std::string 未提供 string(cp, pos2, len2) 这种构造方式,如果代码中使用了该方式,最终会将 cp 指向的数组构造成一个string,然后调用string(s2, pos2, len2)这种构造方式。不提供string(cp, pos2, len2)这种构造方式原因在于:使用这种方式构造容易出现问题,cp是一个指针,通常使用时,能获得其数组长度并检查传入参数;若传入两个参数,容易出现越界。7. 变量初始化变量初始化总是没错的,不管后面是否会修改该值。尤其是int等内建的类型,在类或struct中及容易忽略初始化,使变量成为随机值,产生不可预知的错误。变量请初始化变量请初始化变量请初始化
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签