Windows环境下使用{}而非()初始化导致源码静默运行失败的原因解析
我花了大半天定位到一行导致程序静默崩溃的代码,仅有的差异是成员初始化的写法:
: data(std::move(value)) // 此写法正常运行 : data{std::move(value)} // 此写法导致运行时静默失败
我在MinGW 7(官网下载)和新安装的MinGW64 12.2.0版本中都测试过这段代码,问题复现一致。
问题核心
为什么这两种初始化写法会导致完全不同的运行结果?我原本以为在这个场景下()和{}初始化是等价的。这是编译器bug吗?它似乎横跨了GCC的多个主要版本。
环境说明
- 编译标准:C++17(通过CMake设置)
- 运行环境:Windows 10
源码
头文件 typeerased_original.h
#include <iostream> #include <memory> #include <string> #include <vector> template<typename T> void draw(const T& t, std::ostream& os, std::size_t position) { os << std::string(position, ' ') << t << std::endl; } class Object { public: template<typename T> Object(T value) : p_data(new Model<T>(std::move(value))) { } Object(const Object& object) : p_data(object.p_data->copy()) { } Object(Object&& other) noexcept = default; Object& operator=(const Object& object) { Object tmp(object); *this = std::move(tmp); return *this; } Object& operator=(Object&& object) noexcept = default; friend void draw(const Object& object, std::ostream& os, std::size_t position) { object.p_data->draw_internal(os, position); } private: struct Concept { virtual ~Concept() = default; virtual Concept* copy() const = 0; virtual void draw_internal(std::ostream&, std::size_t) const = 0; }; template<typename T> struct Model : Concept { Model(T value) : data(std::move(value)) // <<<< 此写法正常运行 //: data{std::move(value)} // <<<< 此写法引发问题 { } Concept* copy() const override { return new Model(*this); } void draw_internal(std::ostream& os, std::size_t position) const override { draw(data, os, position); } T data; }; std::unique_ptr<Concept> p_data; }; using Document = std::vector<Object>; void draw(const Document& document, std::ostream& os, std::size_t position) { os << std::string(position, ' ') << "<document>" << std::endl; for(const auto & item : document) { draw(item, os, position + 4); } os << std::string(position, ' ') << "</document>" << std::endl; }
主文件
#include "typeerased_original.h" int main() { std::cout << "start of main" << std::endl; Document document; document.push_back(0); document.push_back(std::string("Hello World")); std::cout << "push back document" << std::endl; document.push_back(document); std::cout << "done" << std::endl; // 程序在此行输出前崩溃 std::cout << "Drawing now!" << std::endl; draw(document, std::cout, 0); std::cout << std::endl; return 0; }
原因分析
这不是编译器bug,而是C++列表初始化规则与代码中隐式转换结合导致的问题:
列表初始化的优先级规则:当使用
{}进行初始化时,编译器会优先匹配类的initializer_list构造函数。对于Document(即std::vector<Object>)来说,它的initializer_list构造函数接受std::initializer_list<Object>。隐式转换的触发:
Object类有一个非explicit的模板构造函数template<typename T> Object(T value),这意味着任何类型(包括std::vector<Object>)都可以隐式转换为Object。当你用data{std::move(value)}初始化时,std::move(value)是std::vector<Object>&&类型,编译器会将其隐式转换为Object,然后用这个Object作为initializer_list<Object>的单个元素,构造出一个仅包含一个元素的std::vector<Object>,而非预期的移动构造原有的document。崩溃的直接原因:当这个构造出的
Object被push_back到原document中时,vector扩容需要拷贝所有元素。拷贝这个特殊的Object时,它内部的data是一个包含自身引用的vector<Object>,会触发无限递归拷贝,最终导致栈溢出、程序崩溃。
而使用data(std::move(value))时,编译器会直接匹配std::vector的移动构造函数,data会正确成为原document的移动版本,不会触发隐式转换和后续的无限递归。
解决方案
- 将
Object的模板构造函数声明为explicit,阻止隐式转换:
此时template<typename T> explicit Object(T value) : p_data(new Model<T>(std::move(value))) { }data{std::move(value)}会正确调用移动构造函数,因为无法再将std::vector<Object>隐式转换为Object,initializer_list构造函数不再匹配。 - 继续使用
()进行成员初始化,避免触发initializer_list构造函数的匹配逻辑。
内容的提问来源于stack exchange,提问作者user2138149

