Move constructor and const member variables

2020-02-26 14:28发布

问题:

I like the idea of const member variables especially when I wrap C functions into classes. The constructor takes a resource handle (e.g. a file descriptor) that stays valid during the whole object life time and the destructor finally closes it. (That is the idea behind RAII, right?)

But with the C++0x move constructor i run into a problem. Since the destructor is also called on the "unloaded" object i need to prevent the cleanup of the resource handle. Since the member variable is const i have no way to assign the value -1 or INVALID_HANDLE (or equivalent values) to indicate to the destructor that it should not do anything.

Is there a way that the destructor is not called if the state of an object was moved to another object?

Example:

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

回答1:

No, there is no way to do this. I would suggest that if you're really attached to the handle variable being const you should have a non-const flag member variable that indicates whether or not destruction should do anything.



回答2:

This is why you should not declare said member variables const. const member variables usually serve no purpose. If you don't want users to mutate the FILE*, then don't provide them with functions to do that, and if you want to stop yourself from mutating it by accident, then mark your functions const. However, do not make member variables themselves const - because then you run into fun when you start to use move or copy semantics.



回答3:

The typical way to implement a move constructor is to zero out or otherwise invalidate the members of the instance being moved (see MSDN for a simple example). Therefore I would say just don't use const here as it is incompatible with the goals of move semantics.



回答4:

Actually, I've run into this problem myself as well today. Not willing to accept 'can't be done' & 'use shared_ptr / reference counting', googling more, I came up with this base class:

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

All methods and constructors are protected, you need to inherit from it to use it. Notice the mutable field: this means that descendant can be a const member in a class. E.g.,

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

Field(s) of A can be const now. Note that I've inherited protected, so that user cannot accidentally cast A to Resource (or very willingly to hack it), but A is still not final, so you can still inherit from this (a valid reason to inherit from a Resource is e.g. to have separate read and read-write access). This is one of the extremely rare cases when protected inheritance doesn't automatically mean that your design is faulty; however, if you find it difficult to understand, you might just use public inheritance.

Then, assuming you have a 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;
    }
};

Note that fields of B can also be const. Our move constructors are const. Given your types have appropriate move constructors, any heap-allocated memory can be shared amongst objects.



回答5:

Reference counting is standard approach that solves your problem. Consider adding reference counting to your class; either manually, or using existing tools like boost shared_ptr.