Wednesday, December 4, 2013

Correcting the transitivity of const in C++

C++ allows you to make an instance of a class const. This normally prevents making changes to any member variables and also prevents the user to call any member functions which are not also marked const.

There is, however, a quirk which you must be aware of. Consider the following code:
struct A {
    int* x;
    A(): x{new int} {}
   ~A() { delete x; }
};

int main()
{
    const A a;
    *a.x = 3;
}
This code compiles and runs with no problems, even though it might appear to be writing over a read-only piece of data.
The type of a.x reveals the reason why this is allowed:
int * const
This denotes a "Constant pointer to int", which is different from a "Pointer to a constant int" or a "Constant pointer to a constant int".
In this case of "Constant pointer to int", overwriting the pointer is not allowed, but overwriting the int it points to is possible.
This may be desired behaviour in some cases, but in others it is not.

This differs from the D Programming Language, where both const and immutable are transitive by default (although in a slightly different but cool way.)

One simple way to prevent the possibly undesired behaviour is to use access modifiers and getters to prevent direct access from the user:
struct A {
    A(): x_{new int} {}
   ~A() { delete x_; }

          int& x()       { return *x_; }
    const int& x() const { return *x_; }
private:
    int* x_;
};
Now it is impossible* to access a mutable reference to *x_ through a const A.
However, it is still possible to write to *x_ from within const member functions of A. This makes it possible for const member functions to have side-effects on the class which are unexpected by the user.
[*]: again, nothing is impossible in C++.

C++11's smart pointers also have the property of not being transitively const.
Note the signatures of these std::unique_ptr member functions:
pointer std::unique_ptr::get() const;

typename std::add_lvalue_reference<T>::type
         operator*() const;  

pointer operator->() const;
These methods all return non-const pointers and references, even if the method is called on a const std::unique_ptr instance.

We could keep enforcing the transitive const relationship by writing the const and non-const getters for every publicly exposed member, but there exists a more general way to solve this problem: We can write a smart pointer with built-in transitivity for const.

std::unique_ptr is not far from the mark, so let's work on top of it:
template<
    class T,
    class Deleter = std::default_delete<T>
> class transitive_ptr : public std::unique_ptr<T,Deleter>
{
public:
    // inherit typedefs for the sake of completeness
    typedef
        typename std::unique_ptr<T,Deleter>::pointer
        pointer;
    typedef
        typename std::unique_ptr<T,Deleter>::element_type
        element_type;
    typedef
        typename std::unique_ptr<T,Deleter>::deleter_type
        deleter_type;

    // extra typedef
    typedef
        const typename std::remove_pointer<pointer>::type*
        const_pointer;

    // inherit std::unique_ptr's constructors
    using std::unique_ptr<T,Deleter>::unique_ptr;
 
    // add transitively const version of get()
    pointer get() {
        return std::unique_ptr<T,Deleter>::get();
    }
    const_pointer get() const {
        return std::unique_ptr<T,Deleter>::get();
    }

    // add transitively const version of operator*()
    typename std::add_lvalue_reference<T>::type
    operator*() {
        return *get();
    }
    typename std::add_lvalue_reference<const T>::type
    operator*() const {
        return *get();
    }
 
    // add transitively const version of operator->()
    pointer operator->() {
        return get();
    }
    const_pointer operator->() const {
        return get();
    }
};
Attempting to write to a transitively const instance of a pointer will now fail, and our class declaration is also much more concise because we were able to convey the rules for using x within its type:
struct A {
    transitive_ptr<int> x;
    A(): x{new int} {}
};

int main() {
    const A a;
    *a.x = 3;
}
Compiler output:
error: assignment of read-only location 
‘a.A::x.transitive_ptr<T, Deleter>::operator*
<int, std::default_delete<int> >()’
*a.x = 3;
     ^
This is another useful smart pointer to add to our smart pointer tool box, getting us one step closer to always correctly enforcing the rule of zero.

Full working example: http://ideone.com/0RUr3V

5 comments:

  1. Or you could write in OO style:

    class A
    {
    public:
    A(): m_x(new int(0)) {}
    ~A() { delete m_x; }

    void setX(int x) { *this->m_x = x; }

    int x() const { return *m_x; }

    private:
    int* m_x;
    };

    int main()
    {
    const A a;
    a.setX(1); // compile error

    return 0;
    }

    and avoid unnecessary complications.

    ReplyDelete
    Replies
    1. valid approach. your call. as "Unknown" pointed out below, doesn't solve the whole problem.

      Delete
  2. Had you been awake while reading the article, you might have noticed this part:

    "However, it is still possible to write to *x_ from within const member functions of A."

    Now show us how to deal with that in an object-oriented way.

    ReplyDelete
  3. "Or you could write in OO style:"
    but your style is not an oo

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete