【问题标题】:Move constructor and const member variables移动构造函数和 const 成员变量
【发布时间】:2011-09-13 03:08:16
【问题描述】:

我喜欢 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;
};

【问题讨论】:

    标签: c++11 raii move-constructor


    【解决方案1】:

    这就是为什么你不应该声明所说的成员变量constconst 成员变量通常无用。如果你不希望用户改变FILE*,那么不要为他们提供这样做的函数,如果你想阻止自己意外地改变它,那么标记你的函数const。但是,不要自己创建成员变量const - 因为当您开始使用移动或复制语义时,您会遇到 fun

    【讨论】:

    • 常量成员变量和引用一样有用。通常你想保存一个值并且你知道你不会改变它,这可能允许编译器进行一些优化。仅当我没有一些可变变量时,将方法标记为 const 才有效。
    • 根据我的经验,它实际上不允许任何有用的编译器优化。它会阻止许多非常有用的语义。
    • 完全正确!我遇到了 fun,因为我将底层类的成员声明为 const 成员。
    • const 字段很有用,因为如果您忘记使用构造函数设置它们,编译器会报错。
    • const 事物的目的与避免全局变量或试图缩小范围的目的相同:减少心理负担。当您知道该字段在整个对象生命周期内是相同的时,您不需要每次在调试器中或重构时都查找它。顺便说一句:Rust 系统语言在这方面采取了更好的方法:它使所有内容都保持不变,并且为了能够改变变量,您必须将其显式声明为 mut。有点可悲的是,C++ 只允许在类中使用 mut,所以你不能通过声明大量的 const 别名来模仿这种行为:c
    【解决方案2】:

    不,没有办法做到这一点。我建议如果你真的附加到 handle 变量是 const 你应该有一个非 const 标志成员变量来指示销毁是否应该做任何事情。

    【讨论】:

    • 你的答案肯定是一条路。但与 C++0x 相关,我不喜欢析构函数必须检查析构函数是否真的应该发生的风格。他们不应该假设对象已完全启动并运行,现在是破坏点吗?
    • @mazatwork:好吧,这样想吧。假设您有一个复杂的对象,它可能处于几种不同的状态,每种状态都需要一组不同的析构函数。例如,有一个可能会或可能不会初始化的缓存,或者可能需要或可能不需要关闭的数据库连接。当您不关闭未在析构函数中打开的数据库连接时,您是否“没有真正破坏”?当然不是。这基本上是一样的。你还在破坏,只是对象所处的状态满足不了多少工作。
    • 为什么不让移动构造函数做一些清理(如果确实有必要的话),这样析构函数就会被真正的破坏。在我看来,这会更合适。因为我们说的是同一个对象,所以双重破坏可能不合理。您的复杂对象示例是我尽量避免使用 RAII 和 DI 等技术的事情。
    • @matzawork:嗯,这就是你的移动构造函数正在做的事情。清理你的对象,这样析构函数就没有任何工作要做。我不明白这个问题。
    【解决方案3】:

    实现移动构造函数的典型方法是将被移动实例的成员置零或以其他方式无效(参见MSDN 的简单示例)。因此我想说不要在这里使用const,因为它与移动语义的目标不兼容。

    【讨论】:

    • 嗯,其实可以使用const,编译器还是会生成move构造函数(当然,如果all requirements满足的话)。这是一个例子作为证明:codepad.org/Xh4va2eR(忽略键盘旧编译器错误..)
    【解决方案4】:

    其实我今天也遇到了这个问题。不愿意接受'不能做'&'使用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;
        }
    };
    

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

    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。请注意,我继承了受保护的,因此用户不会意外地将 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。鉴于您的类型具有适当的移动构造函数,任何堆分配的内存都可以在对象之间共享。

    【讨论】:

      【解决方案5】:

      引用计数是解决您问题的标准方法。考虑为您的课程添加引用计数;手动或使用现有工具,如 boost shared_ptr。

      【讨论】:

      • 这是与所提出问题(处理Files 的移动)的正交主题。您的建议是制作shared_ptr&lt;File&gt;s。可能合适,但可能不合适,而且肯定会涉及更大的设计更改
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-02-22
      • 2013-01-08
      • 1970-01-01
      • 1970-01-01
      • 2014-11-08
      • 2016-01-27
      相关资源
      最近更新 更多