`*this` 可以是 `move()`d 吗?
Can `*this` be `move()`d?
我想定义一个 class 用于编组数据;编组完成后,我想 move
从其中取出编组数据,这可能会使编组对象无效。
我相信这可以通过下面的 static
函数 extractData
实现:
class Marshaller
{
public:
static DataType extractData(Marshaller&& marshaller)
{
return std::move(marshaller.data);
}
private:
DataType data;
}
不过打电话有点不方便:
Marshaller marshaller;
// ... do some marshalling...
DataType marshalled_data{Marshaller::extractData(std::move(marshaller))};
那么我可以用成员函数包装它吗?
DataType Marshaller::toDataType()
{
return Marshaller::extractData(std::move(*this));
}
当然,这会被称为:
DataType marshalled_data{marshaller.toDataType()};
...对我来说,这看起来好多了。但是 std::move(*this)
这件事看起来非常可疑。在调用 toDataType()
的上下文中,marshaller
不能再次使用,但我不认为编译器可以知道:函数体可能在调用者的编译单元之外,所以没有任何迹象表明 marshaller
已经应用了 move()
。
这是未定义的行为吗?完全没问题吗?或者介于两者之间?有没有更好的方法来实现相同的目标,最好不使用宏或要求调用者显式 move
marshaller
?
编辑: 使用 G++ 和 Clang++,我发现我不仅可以编译上述用例,而且实际上我可以 继续修改通过 marshaller 提取基础数据,然后使用 toDataType
函数重新提取修改后的数据。我还发现marshalled_data
中的已经提取的数据继续被marshaller
改变,这表明marshalled_data
在[之间共享=19=] 和调用上下文,所以我怀疑这里存在内存泄漏或未定义的行为(来自双重删除)。
编辑 2: 如果我在 DataType
的析构函数中放置一个 print 语句,当调用者时它会出现 两次 离开范围。如果我在 DataType
中包含一个数据成员,其中有一个数组,对应的 new[]
和 delete[]
,我会得到一个 glibc
"double free or corruption" 错误。所以我不确定这个 怎么会 是安全的,尽管有几个答案说它在技术上是允许的。一个完整的答案应该解释正确使用此技术所需的内容以及重要的 DataType
class.
编辑 3: 这已经足够 rabbit-hole/can-of-worms 我已经打开 来解决我剩下的问题。
根据标准,移出对象仍然有效,尽管其状态无法保证,因此从 *this
移出似乎是完全有效的。它是否会让您的代码用户感到困惑完全是另一个问题。
综上所述,您的 真实 意图是 link 通过提取数据破坏 marshallar。您是否考虑过在单个表达式中完成所有编组并让临时人员为您处理事情?
class Marshaller
{
public:
Marshaller& operator()(input_data data) { marshall(data); return *this; }
DataType operator()() { return std::move(data_); }
private:
DataType data_;
}
DataType my_result = Marshaller()(data1)(data2)(data3)();
您写道,您希望同时销毁编组器并从中删除数据。我真的不会担心尝试同时做这些事情,只需先将数据移出,然后销毁 Marshaller 对象即可。有很多方法可以摆脱 Marshaller 而无需考虑太多,也许智能指针对您有意义?
重构这个的一个选项是为 DataType 提供一个构造函数,该构造函数采用 Marshaller 并将数据移出('friend' 关键字将允许您这样做,因为 DataType 然后将能够达到那个私有 'data' 变量)。
//add this line to the Marshaller
friend class DataType;
struct DataType
{
DataType(Marshaller& marshaller) {
buffer = marshaller.data.buffer;
}
private:
Type_of_buffer buffer;//buffer still needs to know how to have data moved into it
}
你也可以给它一个赋值运算符来做同样的事情(我认为这会起作用:
DataType& operator=(Marshaller&& marshaller) {
this.buffer = std::move(marshaller.data.buffer);
return *this;
}
)
我会避免在 *this 上使用 move,因为即使它是正确的,它也会让人们失望。看起来基于堆栈的缓冲容器可能会给您带来麻烦。
您似乎担心 Marshaller 在编译单元外被再次调用。如果你有密集的并行代码并且正在快速和松散地使用编组器,或者你正在复制指向你的编组器的指针,那么我认为你的担心是有道理的。否则,请查看 Marshaller 是如何移动的,并确保您已经为良好的对象生命周期构建了代码(尽可能使用对象引用)。您也可以只向 marshaller 添加一个成员标志,说明 'data' 是否已被移动,如果有人在它离开后试图访问它(如果您是并行的,请确保锁定),则抛出错误。我只会将此作为最后的手段或快速修复,因为它看起来不对,您的合作开发人员会想知道发生了什么事。
如果你有时间,我有一些问题要挑:
- 您的 extractData 方法缺少静态关键字
- 您在数据类型声明行中混用了方括号和圆括号
我会避免从 *this
移动,但如果你这样做,至少你应该向函数添加右值引用限定符:
DataType Marshaller::toDataType() &&
{
return Marshaller::extractData(std::move(*this));
}
这样,用户将不得不这样调用它:
// explicit move, so the user is aware that the marshaller is no longer usable
Marshaller marshaller;
DataType marshalled_data{std::move(marshaller).toDataType()};
// or it can be called for a temporary marshaller returned from some function
Marshaller getMarshaller() {...}
DataType marshalled_data{getMarshaller().toDataType()};
调用 move(*this)
本身并没有什么不安全的地方。 move
本质上只是一个被调用函数的提示,它可能会窃取对象的内部结构。在类型系统中,这个承诺是通过 &&
引用来表达的。
这与破坏没有任何关系。 move
不执行任何类型的破坏 - 如前所述,它只是使我们能够调用带有 &&
参数的函数。接收移动对象的函数(在本例中为 extractData
)也不做任何破坏。实际上,它需要将对象留在"valid but unspecified state"中。本质上,这意味着必须能够以正常方式销毁对象(通过 delete
或超出范围,具体取决于对象的创建方式)。
所以 - 如果您的 extractData
做了它应该做的事情并使对象处于允许其稍后被破坏的状态 - 就编译器而言,没有任何未定义或危险的事情发生。 users 的代码当然可能存在问题,因为对象正在被移动并不完全明显(并且以后可能不会包含任何数据)。这也许可以通过更改函数名称变得更清楚一些。或者(正如另一个答案所建议的那样)通过&&
-限定整个方法。
我认为您不应该从 *this
移动,而应该从它的 data
字段移动。由于这显然会使 Marshaller
对象处于有效但不可用的状态,因此执行此操作的成员函数本身应该在其隐式 *this
参数上具有右值引用限定符。
class Marshaller
{
public:
...
DataType Marshaller::unwrap() && { return std::move(data); }
...
private:
DataType data;
};
调用它,如果 m
是 Marshaller
变量,如 std::move(m).unwrap()
。不需要任何静态成员来完成此操作。
我想定义一个 class 用于编组数据;编组完成后,我想 move
从其中取出编组数据,这可能会使编组对象无效。
我相信这可以通过下面的 static
函数 extractData
实现:
class Marshaller
{
public:
static DataType extractData(Marshaller&& marshaller)
{
return std::move(marshaller.data);
}
private:
DataType data;
}
不过打电话有点不方便:
Marshaller marshaller;
// ... do some marshalling...
DataType marshalled_data{Marshaller::extractData(std::move(marshaller))};
那么我可以用成员函数包装它吗?
DataType Marshaller::toDataType()
{
return Marshaller::extractData(std::move(*this));
}
当然,这会被称为:
DataType marshalled_data{marshaller.toDataType()};
...对我来说,这看起来好多了。但是 std::move(*this)
这件事看起来非常可疑。在调用 toDataType()
的上下文中,marshaller
不能再次使用,但我不认为编译器可以知道:函数体可能在调用者的编译单元之外,所以没有任何迹象表明 marshaller
已经应用了 move()
。
这是未定义的行为吗?完全没问题吗?或者介于两者之间?有没有更好的方法来实现相同的目标,最好不使用宏或要求调用者显式 move
marshaller
?
编辑: 使用 G++ 和 Clang++,我发现我不仅可以编译上述用例,而且实际上我可以 继续修改通过 marshaller 提取基础数据,然后使用 toDataType
函数重新提取修改后的数据。我还发现marshalled_data
中的已经提取的数据继续被marshaller
改变,这表明marshalled_data
在[之间共享=19=] 和调用上下文,所以我怀疑这里存在内存泄漏或未定义的行为(来自双重删除)。
编辑 2: 如果我在 DataType
的析构函数中放置一个 print 语句,当调用者时它会出现 两次 离开范围。如果我在 DataType
中包含一个数据成员,其中有一个数组,对应的 new[]
和 delete[]
,我会得到一个 glibc
"double free or corruption" 错误。所以我不确定这个 怎么会 是安全的,尽管有几个答案说它在技术上是允许的。一个完整的答案应该解释正确使用此技术所需的内容以及重要的 DataType
class.
编辑 3: 这已经足够 rabbit-hole/can-of-worms 我已经打开
根据标准,移出对象仍然有效,尽管其状态无法保证,因此从 *this
移出似乎是完全有效的。它是否会让您的代码用户感到困惑完全是另一个问题。
综上所述,您的 真实 意图是 link 通过提取数据破坏 marshallar。您是否考虑过在单个表达式中完成所有编组并让临时人员为您处理事情?
class Marshaller
{
public:
Marshaller& operator()(input_data data) { marshall(data); return *this; }
DataType operator()() { return std::move(data_); }
private:
DataType data_;
}
DataType my_result = Marshaller()(data1)(data2)(data3)();
您写道,您希望同时销毁编组器并从中删除数据。我真的不会担心尝试同时做这些事情,只需先将数据移出,然后销毁 Marshaller 对象即可。有很多方法可以摆脱 Marshaller 而无需考虑太多,也许智能指针对您有意义?
重构这个的一个选项是为 DataType 提供一个构造函数,该构造函数采用 Marshaller 并将数据移出('friend' 关键字将允许您这样做,因为 DataType 然后将能够达到那个私有 'data' 变量)。
//add this line to the Marshaller
friend class DataType;
struct DataType
{
DataType(Marshaller& marshaller) {
buffer = marshaller.data.buffer;
}
private:
Type_of_buffer buffer;//buffer still needs to know how to have data moved into it
}
你也可以给它一个赋值运算符来做同样的事情(我认为这会起作用:
DataType& operator=(Marshaller&& marshaller) {
this.buffer = std::move(marshaller.data.buffer);
return *this;
}
)
我会避免在 *this 上使用 move,因为即使它是正确的,它也会让人们失望。看起来基于堆栈的缓冲容器可能会给您带来麻烦。
您似乎担心 Marshaller 在编译单元外被再次调用。如果你有密集的并行代码并且正在快速和松散地使用编组器,或者你正在复制指向你的编组器的指针,那么我认为你的担心是有道理的。否则,请查看 Marshaller 是如何移动的,并确保您已经为良好的对象生命周期构建了代码(尽可能使用对象引用)。您也可以只向 marshaller 添加一个成员标志,说明 'data' 是否已被移动,如果有人在它离开后试图访问它(如果您是并行的,请确保锁定),则抛出错误。我只会将此作为最后的手段或快速修复,因为它看起来不对,您的合作开发人员会想知道发生了什么事。
如果你有时间,我有一些问题要挑:
- 您的 extractData 方法缺少静态关键字
- 您在数据类型声明行中混用了方括号和圆括号
我会避免从 *this
移动,但如果你这样做,至少你应该向函数添加右值引用限定符:
DataType Marshaller::toDataType() &&
{
return Marshaller::extractData(std::move(*this));
}
这样,用户将不得不这样调用它:
// explicit move, so the user is aware that the marshaller is no longer usable
Marshaller marshaller;
DataType marshalled_data{std::move(marshaller).toDataType()};
// or it can be called for a temporary marshaller returned from some function
Marshaller getMarshaller() {...}
DataType marshalled_data{getMarshaller().toDataType()};
调用 move(*this)
本身并没有什么不安全的地方。 move
本质上只是一个被调用函数的提示,它可能会窃取对象的内部结构。在类型系统中,这个承诺是通过 &&
引用来表达的。
这与破坏没有任何关系。 move
不执行任何类型的破坏 - 如前所述,它只是使我们能够调用带有 &&
参数的函数。接收移动对象的函数(在本例中为 extractData
)也不做任何破坏。实际上,它需要将对象留在"valid but unspecified state"中。本质上,这意味着必须能够以正常方式销毁对象(通过 delete
或超出范围,具体取决于对象的创建方式)。
所以 - 如果您的 extractData
做了它应该做的事情并使对象处于允许其稍后被破坏的状态 - 就编译器而言,没有任何未定义或危险的事情发生。 users 的代码当然可能存在问题,因为对象正在被移动并不完全明显(并且以后可能不会包含任何数据)。这也许可以通过更改函数名称变得更清楚一些。或者(正如另一个答案所建议的那样)通过&&
-限定整个方法。
我认为您不应该从 *this
移动,而应该从它的 data
字段移动。由于这显然会使 Marshaller
对象处于有效但不可用的状态,因此执行此操作的成员函数本身应该在其隐式 *this
参数上具有右值引用限定符。
class Marshaller
{
public:
...
DataType Marshaller::unwrap() && { return std::move(data); }
...
private:
DataType data;
};
调用它,如果 m
是 Marshaller
变量,如 std::move(m).unwrap()
。不需要任何静态成员来完成此操作。