无法从initializer_list移动元素的解决方案及C++20/23优化问询
问题本质
initializer_list虽然语法简洁,但存在关键缺陷:无法移动列表中的元素,会导致不必要的拷贝操作。比如用它初始化std::vector时,会触发元素的拷贝构造函数;而直接用push_back传入临时对象时,却能触发更高效的移动构造。
根源在于:initializer_list的迭代器解引用后得到的是const T类型,这意味着我们无法获取元素的非const引用,自然无法调用移动构造函数——编译器只能选择拷贝。
可变参数模板的临时解决方案
目前常用的 workaround 是基于可变参数模板实现一个maker函数,示例如下:
template<typename T, typename... Args> std::vector<T> make_vector(Args&&... args) { std::vector<T> vec; vec.reserve(sizeof...(Args)); (void(vec.push_back(std::forward<Args>(args))), ...); return vec; }
针对这个方案的两个疑问,解答如下:
1. 声明的数组是否会被优化?
这里的“数组”指可变参数展开时可能产生的临时存储结构。在现代编译器(GCC、Clang、MSVC)开启O2及以上优化级别时,这类临时数组会被完全优化掉——编译器会直接将参数逐个转发给push_back,不会在内存中实际创建数组存储中间值,不会带来额外开销。
2. 表达式中的void有何作用?
代码中的void(...)是为了处理折叠表达式的返回值问题:push_back的返回值在不同容器中可能不同(比如部分容器返回迭代器,部分返回void)。用void包裹每个子表达式,可以强制将整个折叠表达式的类型统一为void,避免因返回值类型不匹配导致的编译错误,同时明确表达我们只关心push_back的副作用(插入元素),不依赖其返回值。
C20与C23的更优方案
C++20
C++20的折叠表达式和元组工具可以让临时方案更简洁,比如结合std::apply和std::forward_as_tuple实现完美转发:
template<typename T, typename... Args> auto make_vector(Args&&... args) { return std::apply([](auto&&... elems) { std::vector<T> vec; vec.reserve(sizeof...(elems)); (vec.emplace_back(std::forward<decltype(elems)>(elems)), ...); return vec; }, std::forward_as_tuple(std::forward<Args>(args)...)); }
此外,C++20的std::span也可以配合临时数组使用,但本质和可变参数方案类似,优势是支持范围操作。
C++23
C++23提供了更直接的解决方案,无需手写模板:
std::ranges::to:利用范围库的转换功能,直接将参数序列转换为std::vector,完美转发右值并触发移动构造:#include <ranges> #include <vector> auto vec = std::ranges::to<std::vector<MyType>>(std::move(obj1), std::move(obj2), MyType{});std::make_array配合移动构造:std::make_array在C++23中支持右值参数,返回的数组可以直接移动到std::vector中:#include <array> #include <vector> auto vec = std::vector<MyType>(std::move(std::make_array(std::move(obj1), std::move(obj2))));
这两种方式都避开了initializer_list的const限制,直接实现元素的移动初始化,性能更优。
内容的提问来源于stack exchange,提问作者Gonen I

