不可复制类是否应进行用户转换

时间:2019-03-02 22:34:49

标签: c++ implicit-conversion lvalue noncopyable

我的问题是,不可复制的对象应该具有隐式/显式用户转换吗?至少从下面的示例中,转换看起来非常像副本。

PS:我知道在这里建议将其推荐为“避免隐式转换运算符” https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#c164-avoid-implicit-conversion-operators

为解释我的基本原理,假设我有一个使用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功能将无法接受我的对象。因此,我尝试了总是危险的隐式/显式用户转换路径。

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-我从操作系统获得了HANDLE,并将其传递给我的对象进行管理;
2-我使用隐式用户转换运算符返回了HANDLE;
3-称为我的对象析构函数的编译器关闭了句柄;
4-闭合的手柄以明显的方式泄漏。可能的运行时错误。因为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, ...);
...

因为如果我选择了一元运算符,而不是选择隐式/显式用户转换,则可以避免临时对象执行类似以下操作的转换:

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;
    }
    ...
}

所以回到我的问题,这种情况是否可以避免?或不可复制对象永远不应该具有用户转换功能?老实说,它们与返回某些托管私有对象的简单get()有何不同? 也许是一种避免从临时对象进行用户转换的方法?我们是否可以强制对象在进行其他任何使用之前先进行左值处理,无论是方法调用还是转换?

1 个答案:

答案 0 :(得分:1)

  

这种情况可以避免吗?

不。您正在使用的库功能不会,永远也不会支持您的智能资源管理。真可惜,特别是因为他们没有提供自己的任何东西,但这是现实的情况。

  

还是不可复制对象永远不应该进行用户转换?老实说,它们与返回一些托管私有对象的简单get()有何不同?

.get()对读者来说是一个更大的警告标志,必须谨慎对待以“原始”方式提取的资源的生存期。您的隐式转换编写起来更快,更短,但是不表示这一点,并且更有可能导致错误。

  

也许可以避免用户从临时对象转换?我们是否可以强制对象在进行其他任何使用之前先进行左值处理,无论是方法调用还是转换?

是的,成员函数可以具有ref-qualifiers。尽管如上所述,我认为这并不能真正解决您的总体问题,即无论您想出什么设计,使用这些库函数都需要读者仔细观察。

查看std::unique_ptr。它具有get()函数(以支持要求 a T*的第三方函数),并且没有隐式转换为任何东西(以防止意外发生)。

我的一般建议是避免在可能的情况下进行隐式转换。