开发者

Logical const in D

D has two types of constness: immutable variables are ones that were declared immutable, and always will be immutable, while const variables are simply read only versions of an object.

Logical const is when a function is marked as cons开发者_JS百科t, but allows write access to one or more member variables. The typical use of this is for lazy evaluation, e.g. (in C++)

struct Matrix
{
  double determinant() const
  {
    if ( m_dirty )
    {
      m_determinant = /* expensive calculation */;
      m_dirty = false;
    }
    return m_determinant;
  }

  void set(int i, int j, double x) { m_dirty = true; ...; }

  mutable bool m_dirty;
  mutable double m_determinant;
};

Here, determinant() is const, but can still modify m_dirty and m_determinant due to them being marked as mutable.

The D const(FAQ) says that D2 doesn't support logical const because of the weak guarantee that it provides, which is a hinderance to writing concurrent programs, and makes certain optimisations more difficult.

I completely understand the concern, but what if we need logical const?

Consider the case above with the Matrix class, but without caching (and any need for logical const). Also imagine that this class is used all over my codebase, and is mostly accessed through const references.

Now consider that profiling has revealed that the determinant() function is a bottleneck in the code, and furthermore it is usually accessed repeatedly with its value rarely changing i.e. caching, as above, would be a perfect optimisation.

How can I do that without logical const? Going all over my codebase changing const references to non-const references is not an option (for obvious reasons).

What options do I have (if any)?


I think that it would be appropriate to post the basic conclusions of the recent thread on this topic on the D newsgroup here, so that those who don't track that list can still get the appropriate answer.

D's const is not logical const. It is transitive and fully const. The language does not technically support logical const. The language does not define any way to mutate a const object.

And actually, C++ doesn't have logical const either. Using mutable and casting away const-ness allows you to totally circumvent const, such that, technically-speaking, const doesn't actually guarantee anything except that the you aren't calling any non-const functions on a const variable. The fact that const functions are actually const and don't screw with your variables is completely held together by convention. Now, most programmers don't go around casting away const-ness left and right and making everything mutable, so in practice, it's quite useful, but it not only can be completely circumvented, but the lanuage specifically gives you defined means of doing so. In C++ mutable and casting away const are well-defined and supported by the language.

D doesn't do that. D's const is actually const. Casting away const on a variable and then altering it is undefined. There is no mutable. D's const has real guarantees (as long as you don't do anything undefined like cast away const on something and then mutate it). This is important, not only because the compiler guarantees for D are much stronger than those for C++, but because immutable variables can't be altered in any way shape or form. They could be in read-only memory, and who knows what horrid things would happen if you tried to cast away immutability and alter such a variable (a segfault would likely be the nicest thing that could happen). And since a const variable could actually refer to immutable data, casting away const to alter a variable or allowing for const variables to somehow be altered would be bad, to say the least. So, the language doesn't allow it.

Now, as BCS points out, D is a pragmatic language. You can cast away const, at which point you could alter the variable. So, for instance, you could have a variable which was used to cache the return value of a const function (presumably with that cache being invalidated if the state of the object changed) and cast away const to change it. As long as the variable in question is not actually immutable, it will work. However, this is undefined behavior. Once you do it, you're on your own. You're bypassing the type system and the compiler's guarantees. You are the one responsible for making sure that you don't do it on an immutable object or otherwise screw up what the compiler normally gurantees. So, if you need to do it, you can, but you're stepping out into the Wild West, and it's up to you to make sure that you aren't mutating what you shouldn't.

Given that casting away const will work as long as the variable doesn't actually refer to immutable data, it is possible to create a Mutable template to essentially get what mutable gives you in C++ (so, it'll do the casting away of const-ness for you). he_the_great gives an example of such a template in his answer. But using such a template is still undefined behavior. Using it on an object which is actually immutable is going to cause problems. You, the programmer, must make sure that it's used correctly.

So, D makes it technically possible to have logical const by casting away const, but in order to do it, you have to step outside of what the compiler guarantees by bypassing the type system, and you must make sure that you don't misuse it and mutate variables which shouldn't/can't be mutated, or your code will have problems - segfaults quite possibly being the least among them.

EDIT: I forgot to mention the one proposed solution which does not break the type system. As long as you're willing to forgoe purity, you can use a global variable of some variety (be it at module scope, a class variable, or a struct variable) to hold your cached values. The const function can freely use and mutate the global variables, so it can be used in lieu of the missing mutable. That does mean, however, that the function can't be pure, which could also be a big problem. It is, however, a way to have a const function still be able to mutate the data that it needs to without breaking the type system.


I haven't touched D2 in ages, so you might want to double-check what I say. :)

I'm not sure you really have any good options. D's const and immutable are significantly stronger than C/C++'s, so casting them away isn't an option. You've explicitly ruled out changing your usage of const in your code.

You could cache the result of the operation in a global hashtable keyed on the matrix value itself. That will work for any combination of const/immutable. The problem with that, of course, is that D doesn't have the fastest hashtables in the world and computing the hash could be slow. Maybe pre-compute the hash when creating the matrix.

The other option would be to compute the determinant eagerly when the value changes.

Aside from that, I can't think of anything else. The problem, really, is that you're asking the compiler to protect you with const and then trying to break out of it. The "proper" solution is probably to just not use const. :P


Being a pragmatic language, D has the ability to cast away const if you really need to. I think the following should work:

class M {
  bool set;
  real val;

  real D() const {
    if(!set) {
      M m = cast(M)this;
      m.val = this.some_fn();
      m.set = true;
    }
    return this.val;
  }
}


I highly suggest BCS's answer as it is simple and safe as long as an immutable/const Matrix is not created.

Another option that helps to make even immutable/const objects remain valid is this Mutable Template. Or at least that is the intent. There are comments about the concerns.

With this template, it is required that modification is done to a referenced type and not a value in a const function. This means pointers are used and require space to be allocated for each Matrix. This also makes it harder to create an immutable/const Matrix that won't segfault, maybe there is a way to do it nicely but I only know of one for classes.

struct Matrix
{
    double determinant() const
    {
        if ( *m_dirty )
        {
            *m_determinant = 646.363; /* expensive calculation */;
            *m_dirty = false;
        }
        return *m_determinant;
    }

    void set(int i, int j, double x) { *m_dirty = true; }

    Mutable!(bool*) m_dirty;
    Mutable!(double*) m_determinant;
};


To mimic logical const from C++ in D you can use inheritance for classes:

class ConstMatrix
{
public:
    double det() { // not marked as const!
        /* ... caching code ... */
    }
    /* ... the rest of the logically const interface ... */
}

class Matrix : ConstMatrix
{
public:
    void set( int row, int col, double val ) {
        /* ... */
    }
    /* ... the non-logically const interface ... */
}

In the ConstMatrix class implementation you won't have any compiler checks unless you put const qualifiers onto the function signatures. However, you will get const correctness for client code, if you use ConstMatrix for logically constant matrices.

For structs you can use the alias technique to accomplish the same:

struct ConstMatrix 
{
    /* logically const bla bla blub */
}

struct Matrix
{
public:
    alias m this;

    /* non-const bla bla blub */
private:
    ConstMatrix m;
}

For class types you can build other classes or structs with logical const correctness in the following way:

class ConstBiggerClass
{
private:
    ConstMatrix m;
}

class BiggerClass : ConstBiggerClass
{
private:
    Matrix m;
}

This way the compiler will check const correctness for you. However, you'll need an extra data member. For class type members in class types, an alternative would be to provide a member function which returns the data member with the right constness:

class ConstBiggerClass
{
public:
    void someLogicallyConstFunction( /*...*/ ) { /* ... */ }
protected:
    abstract ConstMatrix getMatrix();
}

class BiggerClass : ConstBiggerClass
{
public:
    void someMutableFunction( /*...*/ ) { /*...*/ }
protected:
    Matrix getMatrix() { return m; }
private:
    Matrix m;
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜