Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 const, 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)?

like image 831
Peter Alexander Avatar asked Nov 18 '10 21:11

Peter Alexander


2 Answers

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.

like image 102
Jonathan M Davis Avatar answered Oct 03 '22 16:10

Jonathan M Davis


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

like image 26
DK. Avatar answered Oct 03 '22 16:10

DK.