如何根据构造器输入初始化std::variant类成员变量
C++17中std::variant根据运行时参数初始化的正确实现
问题描述
我有一个BASE类,其中包含类型为std::variant<A, B>的成员变量myVar,该变量可存储A类或B类对象。希望在运行时根据构造器的输入,调用A或B的带参构造函数来初始化该变量。
尝试过两种方式:
- 第一种:在构造函数内通过条件判断赋值,但赋值后临时对象的析构函数会被调用,导致
myVar存储垃圾数据; - 第二种:尝试用初始化列表,但条件表达式的写法无法通过编译。
附尝试代码:
#include <iostream> #include <string> #include <variant> class A { private: char* arr; public: A() = default; A(int n) { arr = new char[3]; arr[0] = arr[1] = arr[2] = 'A'; std::cout << "Constructor A called with " + std::to_string(n); std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; } ~A() { std::cout << "Destructor A called"; std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; delete[] arr; } }; class B { private: char* arr; public: B() = default; B(int n) { arr = new char[3]; arr[0] = arr[1] = arr[2] = 'B'; std::cout << "Constructor B called with " + std::to_string(n); std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; } ~B() { std::cout << "Destructor B called "; std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; delete[] arr; } }; class BASE { private: std::variant<A, B> myVar; public: BASE() = delete; BASE(int i) { if (i == 0) { std::cout << "Initializing A" << std::endl; myVar = A(i); } else { std::cout << "Initializing B" << std::endl; myVar = B(i); } } }; int main() { BASE test1(0); }
问题根源
第一种方法出现垃圾数据的核心原因是:A和B类没有定义正确的拷贝/移动构造函数,编译器生成的默认版本是浅拷贝。当执行myVar = A(i)时,临时A对象的arr指针会被拷贝到variant存储的A对象中,临时对象析构时会释放arr指向的内存,导致variant里的A对象持有野指针,访问时出现非法数据。
解决方案
方案1:修复A和B的拷贝/移动语义
给A和B添加移动构造函数(或拷贝构造函数),实现资源所有权转移或深拷贝,避免野指针问题:
#include <iostream> #include <string> #include <variant> #include <utility> class A { private: char* arr; public: A() = default; A(int n) { arr = new char[3]; arr[0] = arr[1] = arr[2] = 'A'; std::cout << "Constructor A called with " + std::to_string(n); std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; } // 移动构造函数:转移资源所有权 A(A&& other) noexcept : arr(other.arr) { other.arr = nullptr; // 避免原对象析构时释放内存 std::cout << "Move Constructor A called" << std::endl; } // 禁用拷贝构造(若不需要拷贝功能) A(const A&) = delete; A& operator=(const A&) = delete; // 移动赋值运算符 A& operator=(A&& other) noexcept { if (this != &other) { delete[] arr; arr = other.arr; other.arr = nullptr; } std::cout << "Move Assignment A called" << std::endl; return *this; } ~A() { if (arr) { std::cout << "Destructor A called"; std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; delete[] arr; } else { std::cout << "Destructor A called (nullptr)" << std::endl; } } }; class B { private: char* arr; public: B() = default; B(int n) { arr = new char[3]; arr[0] = arr[1] = arr[2] = 'B'; std::cout << "Constructor B called with " + std::to_string(n); std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; } // 移动构造函数 B(B&& other) noexcept : arr(other.arr) { other.arr = nullptr; std::cout << "Move Constructor B called" << std::endl; } // 禁用拷贝构造 B(const B&) = delete; B& operator=(const B&) = delete; // 移动赋值运算符 B& operator=(B&& other) noexcept { if (this != &other) { delete[] arr; arr = other.arr; other.arr = nullptr; } std::cout << "Move Assignment B called" << std::endl; return *this; } ~B() { if (arr) { std::cout << "Destructor B called "; std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; delete[] arr; } else { std::cout << "Destructor B called (nullptr)" << std::endl; } } }; class BASE { private: std::variant<A, B> myVar; public: BASE() = delete; BASE(int i) { if (i == 0) { std::cout << "Initializing A" << std::endl; myVar = A(i); // 调用移动赋值,转移资源所有权 } else { std::cout << "Initializing B" << std::endl; myVar = B(i); } } }; int main() { BASE test1(0); }
方案2:在初始化列表中直接构造variant(避免临时对象)
如果不想修改A和B的结构,可以通过辅助函数在初始化列表中直接构造std::variant,从根源上避免临时对象析构的问题:
#include <iostream> #include <string> #include <variant> class A { private: char* arr; public: A() = default; A(int n) { arr = new char[3]; arr[0] = arr[1] = arr[2] = 'A'; std::cout << "Constructor A called with " + std::to_string(n); std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; } ~A() { std::cout << "Destructor A called"; std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; delete[] arr; } }; class B { private: char* arr; public: B() = default; B(int n) { arr = new char[3]; arr[0] = arr[1] = arr[2] = 'B'; std::cout << "Constructor B called with " + std::to_string(n); std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; } ~B() { std::cout << "Destructor B called "; std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; delete[] arr; } }; class BASE { private: std::variant<A, B> myVar; // 辅助函数:根据参数直接构造对应类型的variant static std::variant<A, B> createVar(int i) { if (i == 0) { return std::variant<A, B>(std::in_place_type<A>, i); } else { return std::variant<A, B>(std::in_place_type<B>, i); } } public: BASE() = delete; // 初始化列表中直接构造myVar,无临时对象 BASE(int i) : myVar(createVar(i)) { std::cout << "Initializing " << (i == 0 ? "A" : "B") << std::endl; } }; int main() { BASE test1(0); }
这个方案通过std::in_place_type直接在variant内部构造A或B对象,不需要创建临时对象,彻底避免了野指针问题。
内容的提问来源于stack exchange,提问作者pisoir
相关产品推荐
相关产品推荐

