移动构造函数和常量成员变量

35

我喜欢const成员变量的想法,尤其是当我将C函数包装成类时。构造函数接受一个资源句柄(例如文件描述符),它在整个对象生命周期内保持有效,并且析构函数最终关闭它。(这就是RAII的思想,对吧?)

但是使用C++0x移动构造函数时,我遇到了一个问题。由于析构函数也在“卸载”的对象上调用,我需要防止清理资源句柄。由于成员变量是const,我无法赋值为-1或INVALID_HANDLE(或等效值)以指示析构函数不应执行任何操作。

是否有一种方法可以在对象状态被移动到另一个对象时不调用析构函数?

示例:

class File
{
public:
    // Kind of "named constructor" or "static factory method"
    static File open(const char *fileName, const char *modes)
    {
        FILE *handle = fopen(fileName, modes);
        return File(handle);
    }

private:
    FILE * const handle;

public:
    File(FILE *handle) : handle(handle)
    {
    }

    ~File()
    {
        fclose(handle);
    }

    File(File &&other) : handle(other.handle)
    {
        // The compiler should not call the destructor of the "other"
        // object.
    }

    File(const File &other) = delete;
    File &operator =(const File &other) = delete;
};
5个回答

29
这就是为什么不应该声明成员变量为constconst成员变量通常没有任何作用。如果您不希望用户更改FILE*,则不要向他们提供执行此操作的函数;如果您想防止自己意外更改它,请将函数标记为const。但是,不要将成员变量本身设置为const - 因为这时在使用移动或复制语义时会遇到一些麻烦。

32
像引用一样,const成员变量也很有用。通常,您希望保留某个值,并且知道自己不会更改它,这可能允许编译器进行一些优化。将方法标记为const只有在没有可变变量的情况下才有效。 - mazatwork
8
根据我的经验,它并不允许任何有用的编译器优化,并且阻止了许多非常有用的语义。 - Puppy
1
确实如此!我遇到了问题,因为我将底层类的成员声明为const成员。 - Leedehai
6
const 字段很有用,因为编译器可以在你忘记使用构造函数初始化它们时给出警告。 - Michael Krebs
7
const 的作用和避免全局变量或尝试减少范围的目的相同:减少心理负担。当您知道字段在整个对象生命周期中保持不变时,您无需在调试器中或重构时每次查找它。顺便说一句:系统语言 Rust 在这方面采取了更好的方法:它将所有内容都声明为常量,要能够改变变量,必须明确声明为 mut。有点遗憾的是 C++ 只允许在类中使用 mut,因此您无法通过声明大量的 const 别名来模仿这种行为 :c - Hi-Angel
这让我觉得是语言退步。我们不应该为了使用移动语义而牺牲成员变量的常量语义。 - undefined

10

不行,没有办法这样做。如果你非常想让handle变量成为const,我建议你添加一个非const标志成员变量,来指示是否需要执行销毁操作。


1
你的答案无疑是可行的方式。但是关于C++0x,我不喜欢析构函数必须检查是否真正需要析构的风格。它们不应该假设对象完全处于运行状态,并且现在是进行销毁的时刻吗? - mazatwork
2
@mazatwork:好吧,这样想。假设你有一个复杂的对象,它可以处于几种不同的状态,每个状态都需要不同的析构函数集。例如,有一个可能已初始化或未初始化的缓存,或者可能需要关闭的数据库连接。如果您在析构函数中没有关闭未打开的数据库连接,那么您是否“不真正地进行了析构”?当然不是。这基本上是一样的。您仍然在进行析构,只是对象所处的状态并不需要太多工作。 - Omnifarious
为什么不让移动构造函数做一些清理工作(如果实际上需要),这样析构函数就可以留下真正的销毁。在我看来,这会更好。因为我们谈论的是同一个对象,双重销毁可能不合理。您提到的复杂对象示例是我尝试避免使用RAII和DI技术的事情。 - mazatwork
1
@matzawork:嗯,这就是你的移动构造函数所做的事情。清理你的对象,以便析构函数没有任何工作要做。我不明白问题在哪里。 - Omnifarious

5

实际上,我今天也遇到了这个问题。不想接受“无法完成”和“使用shared_ptr /引用计数”的说法,我通过搜索找到了这个基类:

class Resource
{
private:
     mutable bool m_mine;

protected:
    Resource()
    : m_mine( true )
    {
    }

    Resource(const Resource&)       = delete;
    void operator=(const Resource&) = delete;

    Resource(const Resource&& other)
    : m_mine( other.m_mine )
    {
        other.m_mine = false;
    }

    bool isMine() const
    {
        return m_mine;
    }
};

所有方法和构造函数都是受保护的,您需要继承它才能使用它。请注意可变字段:这意味着后代可以成为类中的常量成员。例如:

class A : protected Resource
{
private:
    const int m_i;

public:
    A()
    : m_i( 0 )
    {
    }

    A( const int i )
    : m_i( i )
    {
    }

    A(const A&& a)
    : Resource( std::move( a     ) )
    , m_i     ( std::move( a.m_i ) ) // this is a move iff member has const move constructor, copy otherwise
    {
    }

    ~A()
    {
        if ( isMine() )
        {
            // Free up resources. Executed only for non-moved objects
            cout << "A destructed" << endl;
        }
    }
};

A现在可以使用const字段。请注意,我已经继承了protected,所以用户不能意外地将A强制转换为Resource(或者非常愿意黑掉它),但是A仍然不是最终版本,因此您仍然可以从中继承(从Resource继承的一个有效原因是例如拥有单独的读取和读写访问)。这是极其罕见的情况之一,其中受保护的继承并不自动意味着您的设计有错误;但是,如果您发现难以理解,您可能只需要使用公共继承。

然后,假设您有一个struct X

struct B
{
    const A m_a;
    const X m_x;

    B(const A&& a, const X& x) // implement this way only if X has copy constructor; otherwise do for 'x' like we do for 'a'
    : m_a( std::move( a ) )
    , m_x(            x   )
    {
    }

    B( const B&& b )
    : m_a( std::move( b.m_a ) )
    , m_x( std::move( b.m_x ) ) // this is a move iff X has move constructor, copy otherwise
    {
    }

    ~B()
    {
        cout << "B destructed" << endl;
    }
};

请注意,B的字段也可以是const。我们的移动构造函数是const。如果你的类型有适当的移动构造函数,任何堆分配的内存都可以在对象之间共享。

4
实现移动构造函数的典型方法是将正在移动的实例的成员清零或以其他方式使其无效(有关简单示例,请参见MSDN)。因此,我会建议在此处不要使用const,因为它与移动语义的目标不兼容。

1
好的,实际上你可以使用const,编译器仍然会生成移动构造函数(当然,如果满足所有要求)。以下是一个证明的例子:codepad.org/Xh4va2eR(忽略codepad旧编译器的错误..) - Antonio Barreto

-1

引用计数是解决您问题的标准方法。考虑向您的类添加引用计数,可以手动实现,也可以使用现有工具,如boost shared_ptr。


2
这是一个与所提出的问题(即处理“文件”移动)无关的正交话题。您提出的是创建“shared_ptr<File>”。可能是合适的,但也可能不是,并且肯定会涉及更大的设计更改。 - Steven Lu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接