Non-Copyable class 是否应该有用户转化
Should Non-Copyable class have user conversion
我的问题是,不可复制的对象是否应该有 implicit/explicit 用户转换?至少从我下面的示例来看,转换看起来非常像副本。
PS: 我知道这里推荐给 "Avoid implicit conversion operators" https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#c164-avoid-implicit-conversion-operators
为了解释我的理由,假设我有一个 class 使用 RAII 管理 OS 对象。没什么特别的。
struct unique_handle
{
unique_handle(HANDLE h) : handle(h) {}
//NON COPYABLE
unique_handle(const unique_handle&) = delete;
unique_handle& operator=(const unique_handle&) = delete;
//MOVEABLE
unique_handle(unique_handle&& other) : handle(other.handle)
{
other.handle = INVALID_HANDLE_VALUE;
}
unique_handle& operator=(unique_handle&& other)
{
if (this != &other) {
handle = other.handle;
other.handle = INVALID_HANDLE_VALUE;
}
return *this;
}
~unique_handle()
{
if(handle != INVALID_HANDLE_VALUE)
::CloseHandle(handle);
}
private:
HANDLE handle;
}
没关系。它允许我做类似的事情:
namespace w32
{
unique_handle CreateFile(...)
{
HANDLE handle = ::CreateFileA(...);
return { handle };
}
}
问题是其他 OS 函数不接受我的对象。因此,我尝试了 implicit/explicit 用户转化的始终危险的路径。
struct unique_handle
{
...
operator HANDLE() const noexcept { return handle; }
...
}
这让我可以正常使用我的对象。
...
auto dirHandle = w32::CreateFile(...); // my function returning my object
::ReadDirectoryChangesW(dirHandle, // my object on a native OS call
...);
...
太棒了!但现在的问题是我允许泄漏关闭句柄的可能性。假设我直接将我的对象分配给一个 HANDLE 对象,从不对其进行左值化,这不仅会泄漏非托管 HANDLE,甚至会泄漏一个已关闭的 HANDLE。例如:
Problem 1
...
HANDLE h = w32::CreateFile(...);
...
我了解到问题 1 发生了以下情况:
1 - 我从 OS 得到了 HANDLE 并将它传递给我的对象来管理它;
2 - 我使用隐式用户转换运算符返回了句柄;
3 - 编译器调用了关闭句柄的对象析构函数;
4 - 闭合的 HANDLE 以不明显的方式泄漏。可能的运行时错误。因为 HANDLE 不是 INVALID_HANDLE_VALUE,所以甚至错误检查也无法捕捉到这一点。
当然我也启用了这个案例。但是为了争论,让我们说我接受这个案子。
Problem 2
HANDLE h1 = ...;
{
auto h2 = w32::CreateFile(...); // my method and my object
h1 = h2; // implicit cast to the OS HANDLE
} // our favorite feature closing the HANDLE
::ReadDirectoryChangesW(h1, ...);
...
因为如果我没有选择 implicit/explicit 用户转换,而是选择一元运算符,我本可以避免临时对象的转换,例如:
HANDLE operator !(unique_handle& v2)
{
return v2.handle;
}
这让我在以下行出现编译错误:
...
HANDLE h = !w32::CreateFile(...);
...
这是理想的,但据我所知,我们不能对转化做同样的事情。
我想象的其他解决方案是函数,例如:
struct unique_handle
{
...
// just return - In my opinion also a copy, but what can we make?
[[nodiscard]] HANDLE get() const noexcept { return handle; }
// Stop managing this HANDLE
[[nodiscard]] HANDLE release() noexcept
{
auto h = handle;
handle = INVALID_HANDLE_VALUE;
return h;
}
...
}
所以回到我的问题,这种情况是可以避免的吗?或者 Non-Copyable 对象不应该有用户转换?老实说,它们与 returns 一些托管私有对象的简单 get() 有何不同?
也许是一种避免用户从临时对象转换的方法?我们能否强制对象在任何其他用途之前被左值化,无论是方法调用还是转换?
is this scenario avoidable?
没有。您正在使用的库函数不会、不能也永远不会支持您的智能资源管理。真可惜,特别是因为他们不提供自己的任何东西,但这是现实情况。
Or Non-Copyable objects should never have user conversion? And to be honest, how are they different from a simple get() that returns some managed private object?
.get()
是 reader 的一个更大的警告标志,必须注意以 "raw" 方式提取的资源的生命周期。您的隐式转换编写起来更快更短,但不要发出信号,并且更有可能导致错误。
Maybe a way to avoid user conversions from temporary objects? Can we force a object to be lvalue-ed before any other use, be it a method call or a conversion?
是的,一个成员函数可以有ref-qualifiers。尽管如上所述,我认为这并不能真正解决您的首要问题,即 每次 使用这些库函数都需要 reader 仔细观察,无论你想出了什么设计。
看看std::unique_ptr
。它有一个 get()
函数(支持 要求 一个 T*
的第三方函数)并且没有任何隐式转换(以防止意外发生)。
我的一般建议是尽可能避免隐式转换。
我的问题是,不可复制的对象是否应该有 implicit/explicit 用户转换?至少从我下面的示例来看,转换看起来非常像副本。
PS: 我知道这里推荐给 "Avoid implicit conversion operators" https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#c164-avoid-implicit-conversion-operators
为了解释我的理由,假设我有一个 class 使用 RAII 管理 OS 对象。没什么特别的。
struct unique_handle
{
unique_handle(HANDLE h) : handle(h) {}
//NON COPYABLE
unique_handle(const unique_handle&) = delete;
unique_handle& operator=(const unique_handle&) = delete;
//MOVEABLE
unique_handle(unique_handle&& other) : handle(other.handle)
{
other.handle = INVALID_HANDLE_VALUE;
}
unique_handle& operator=(unique_handle&& other)
{
if (this != &other) {
handle = other.handle;
other.handle = INVALID_HANDLE_VALUE;
}
return *this;
}
~unique_handle()
{
if(handle != INVALID_HANDLE_VALUE)
::CloseHandle(handle);
}
private:
HANDLE handle;
}
没关系。它允许我做类似的事情:
namespace w32
{
unique_handle CreateFile(...)
{
HANDLE handle = ::CreateFileA(...);
return { handle };
}
}
问题是其他 OS 函数不接受我的对象。因此,我尝试了 implicit/explicit 用户转化的始终危险的路径。
struct unique_handle
{
...
operator HANDLE() const noexcept { return handle; }
...
}
这让我可以正常使用我的对象。
...
auto dirHandle = w32::CreateFile(...); // my function returning my object
::ReadDirectoryChangesW(dirHandle, // my object on a native OS call
...);
...
太棒了!但现在的问题是我允许泄漏关闭句柄的可能性。假设我直接将我的对象分配给一个 HANDLE 对象,从不对其进行左值化,这不仅会泄漏非托管 HANDLE,甚至会泄漏一个已关闭的 HANDLE。例如:
Problem 1
...
HANDLE h = w32::CreateFile(...);
...
我了解到问题 1 发生了以下情况:
1 - 我从 OS 得到了 HANDLE 并将它传递给我的对象来管理它;
2 - 我使用隐式用户转换运算符返回了句柄;
3 - 编译器调用了关闭句柄的对象析构函数;
4 - 闭合的 HANDLE 以不明显的方式泄漏。可能的运行时错误。因为 HANDLE 不是 INVALID_HANDLE_VALUE,所以甚至错误检查也无法捕捉到这一点。
当然我也启用了这个案例。但是为了争论,让我们说我接受这个案子。
Problem 2
HANDLE h1 = ...;
{
auto h2 = w32::CreateFile(...); // my method and my object
h1 = h2; // implicit cast to the OS HANDLE
} // our favorite feature closing the HANDLE
::ReadDirectoryChangesW(h1, ...);
...
因为如果我没有选择 implicit/explicit 用户转换,而是选择一元运算符,我本可以避免临时对象的转换,例如:
HANDLE operator !(unique_handle& v2)
{
return v2.handle;
}
这让我在以下行出现编译错误:
...
HANDLE h = !w32::CreateFile(...);
...
这是理想的,但据我所知,我们不能对转化做同样的事情。
我想象的其他解决方案是函数,例如:
struct unique_handle
{
...
// just return - In my opinion also a copy, but what can we make?
[[nodiscard]] HANDLE get() const noexcept { return handle; }
// Stop managing this HANDLE
[[nodiscard]] HANDLE release() noexcept
{
auto h = handle;
handle = INVALID_HANDLE_VALUE;
return h;
}
...
}
所以回到我的问题,这种情况是可以避免的吗?或者 Non-Copyable 对象不应该有用户转换?老实说,它们与 returns 一些托管私有对象的简单 get() 有何不同? 也许是一种避免用户从临时对象转换的方法?我们能否强制对象在任何其他用途之前被左值化,无论是方法调用还是转换?
is this scenario avoidable?
没有。您正在使用的库函数不会、不能也永远不会支持您的智能资源管理。真可惜,特别是因为他们不提供自己的任何东西,但这是现实情况。
Or Non-Copyable objects should never have user conversion? And to be honest, how are they different from a simple get() that returns some managed private object?
.get()
是 reader 的一个更大的警告标志,必须注意以 "raw" 方式提取的资源的生命周期。您的隐式转换编写起来更快更短,但不要发出信号,并且更有可能导致错误。
Maybe a way to avoid user conversions from temporary objects? Can we force a object to be lvalue-ed before any other use, be it a method call or a conversion?
是的,一个成员函数可以有ref-qualifiers。尽管如上所述,我认为这并不能真正解决您的首要问题,即 每次 使用这些库函数都需要 reader 仔细观察,无论你想出了什么设计。
看看std::unique_ptr
。它有一个 get()
函数(支持 要求 一个 T*
的第三方函数)并且没有任何隐式转换(以防止意外发生)。
我的一般建议是尽可能避免隐式转换。