为什么我的变体 return 的值不等于分配给它的值?
Why does my variant return a value not equal to what it was assigned?
我正在尝试创建一个简单的变体作为学习练习。
我想在不动态分配内存的情况下执行此操作,正如 std::variant
的 c++ 规范所指定的那样。
为简化起见,我的变体只能取两个值。
这是我的实现:
//variant style class for two types
template<typename T1, typename T2>
class Either {
using Bigest = std::conditional<sizeof(T1) >= sizeof(T2), T1, T2>;
using ByteArray = std::array<std::byte, sizeof(Bigest)>;
ByteArray val;
std::optional<std::type_index> containedType;
public:
Either() : containedType(std::nullopt) {}
template<typename T>
Either(const T& actualVal) : containedType(typeid(T)) { //ToDo check T is one of correct types
ByteArray* ptr = (ByteArray*)&actualVal;
val = *ptr;
}
class BadVariantAccess {};
template<typename T>
inline T& getAs() const {
if(containedType == typeid(T)) {
T* ptr = (T*)val.data();
return *ptr;
}
else throw BadVariantAccess();
}
};
但是,当我测试这个时,我在尝试获取值后得到了一个错误的数字:
int main() {
Either<int,float> e = 5;
std::cout << e.getAs<int>() << std::endl;
return 0;
}
Returns 一个随机数(例如 272469509)。
我的实施有什么问题,我该如何解决?
ByteArray* ptr = (ByteArray*)&actualVal;
这是荒谬的。您是说 ptr
指向 std::array
,但事实并非如此。当然,如果你取消引用它,你会得到垃圾。
您的程序由于各种原因表现出未定义的行为,因此试图解释为什么您会观察到给定的行为有点毫无意义。但是您的代码存在一些严重问题:
将任何内存别名为 std::array<std::byte, N>
是不安全的。这违反了严格别名规则,唯一的例外是指向 char
和 std::byte
的指针。在执行 ByteArray* ptr = (ByteArray*)&actualVal; val = *ptr;
时,您正在调用 std::array's
复制构造函数并传递一个不存在的实例。在这一点上,几乎任何事情都可能发生。您应该改为逐个复制字节(对于普通类型),或者使用放置 new
在基于字节的存储中复制构造对象。
您的存储未对齐,这可能会导致运行时崩溃或严重的性能损失,具体取决于您的目标平台。我不确定这是否会导致未定义的行为,但如果您想继续这种低级内存管理,您一定要解决这个问题。
您的复制构造函数不检查分配类型在内存中的实际大小。如果在某些平台上,您有 sizeof(int) > sizeof(float)
,那么当您从 float
复制构造一个 Either<int, float>
时,您将读取超过浮点数末尾的字节,并且很容易导致未定义的行为。您必须考虑分配的类型的大小
如果您打算存储除琐碎类型以外的任何内容(即 std::string
或 std::vector
,而不仅仅是基元),您需要调用适当的 copy/move constructor/assignment 运算符。对于构造函数(默认、移动、复制),您需要使用放置 new
在预分配存储中构造活动对象。此外,您需要使用类型擦除来存储一些函数,这些函数会将包含的对象破坏为正确的类型。 std::function<void(std::byte*)>
中的 lambda 在这里可能非常有用,它只调用析构函数: [](std::byte* data){ (*reinterpret_cast<T*>(data)).~T(); }
每当您存储新类型时都必须分配它。请注意,这种情况几乎是您唯一一次想要手动调用析构函数的情况。
我强烈建议您仔细阅读有关如何正确执行此类低级内存管理的内容。很容易做错并且一无所知,直到后来你被奇怪的错误所困扰。
正如@MilesBudnek 所指出的那样,using Biggest = std::conditional<sizeof(T1) >= sizeof(T2), T1, T2>
将给出类型 trait,因此 Biggest
的实例实际上是一个实例结构 std::conditional
和 not T1
或 T2
的特化。您的意思可能是 std::conditional_t<...>
或 std::conditional<...>::type;
这可能会导致您的 Either
class 只会分配一个字节,这显然是不正确的。
我正在尝试创建一个简单的变体作为学习练习。
我想在不动态分配内存的情况下执行此操作,正如 std::variant
的 c++ 规范所指定的那样。
为简化起见,我的变体只能取两个值。
这是我的实现:
//variant style class for two types
template<typename T1, typename T2>
class Either {
using Bigest = std::conditional<sizeof(T1) >= sizeof(T2), T1, T2>;
using ByteArray = std::array<std::byte, sizeof(Bigest)>;
ByteArray val;
std::optional<std::type_index> containedType;
public:
Either() : containedType(std::nullopt) {}
template<typename T>
Either(const T& actualVal) : containedType(typeid(T)) { //ToDo check T is one of correct types
ByteArray* ptr = (ByteArray*)&actualVal;
val = *ptr;
}
class BadVariantAccess {};
template<typename T>
inline T& getAs() const {
if(containedType == typeid(T)) {
T* ptr = (T*)val.data();
return *ptr;
}
else throw BadVariantAccess();
}
};
但是,当我测试这个时,我在尝试获取值后得到了一个错误的数字:
int main() {
Either<int,float> e = 5;
std::cout << e.getAs<int>() << std::endl;
return 0;
}
Returns 一个随机数(例如 272469509)。
我的实施有什么问题,我该如何解决?
ByteArray* ptr = (ByteArray*)&actualVal;
这是荒谬的。您是说 ptr
指向 std::array
,但事实并非如此。当然,如果你取消引用它,你会得到垃圾。
您的程序由于各种原因表现出未定义的行为,因此试图解释为什么您会观察到给定的行为有点毫无意义。但是您的代码存在一些严重问题:
将任何内存别名为
std::array<std::byte, N>
是不安全的。这违反了严格别名规则,唯一的例外是指向char
和std::byte
的指针。在执行ByteArray* ptr = (ByteArray*)&actualVal; val = *ptr;
时,您正在调用std::array's
复制构造函数并传递一个不存在的实例。在这一点上,几乎任何事情都可能发生。您应该改为逐个复制字节(对于普通类型),或者使用放置new
在基于字节的存储中复制构造对象。您的存储未对齐,这可能会导致运行时崩溃或严重的性能损失,具体取决于您的目标平台。我不确定这是否会导致未定义的行为,但如果您想继续这种低级内存管理,您一定要解决这个问题。
您的复制构造函数不检查分配类型在内存中的实际大小。如果在某些平台上,您有
sizeof(int) > sizeof(float)
,那么当您从float
复制构造一个Either<int, float>
时,您将读取超过浮点数末尾的字节,并且很容易导致未定义的行为。您必须考虑分配的类型的大小如果您打算存储除琐碎类型以外的任何内容(即
std::string
或std::vector
,而不仅仅是基元),您需要调用适当的 copy/move constructor/assignment 运算符。对于构造函数(默认、移动、复制),您需要使用放置new
在预分配存储中构造活动对象。此外,您需要使用类型擦除来存储一些函数,这些函数会将包含的对象破坏为正确的类型。std::function<void(std::byte*)>
中的 lambda 在这里可能非常有用,它只调用析构函数:[](std::byte* data){ (*reinterpret_cast<T*>(data)).~T(); }
每当您存储新类型时都必须分配它。请注意,这种情况几乎是您唯一一次想要手动调用析构函数的情况。
我强烈建议您仔细阅读有关如何正确执行此类低级内存管理的内容。很容易做错并且一无所知,直到后来你被奇怪的错误所困扰。
正如@MilesBudnek 所指出的那样,using Biggest = std::conditional<sizeof(T1) >= sizeof(T2), T1, T2>
将给出类型 trait,因此 Biggest
的实例实际上是一个实例结构 std::conditional
和 not T1
或 T2
的特化。您的意思可能是 std::conditional_t<...>
或 std::conditional<...>::type;
这可能会导致您的 Either
class 只会分配一个字节,这显然是不正确的。