How to rewrite C++ code that uses mutable in D?
Asked Answered
T

3

6

If you needed to rewrite the following C++ code in D, how would you do it?

struct A{

    const S* _s;
    B _b;
    C _c;
    mutable C _c1, _c2;

    A(const B& b, const C& c, const S* s){ /*...*/ }

    void compute(const R& r) const
    {
      //...
      _c1 = ...
      _c2 = ...
    }
};

D doesn't have mutable, and, based on my experience, it's rarely used in C++. But, assuming mutable is used for the right reasons here, what are my options in D?

Thinner answered 30/12, 2011 at 5:36 Comment(1)
This is a similar question: https://mcmap.net/q/870995/-logical-const-in-dHalonna
U
3

You have three options to deal with this:

  1. Cast away the const. This will shit the compiler up, but there's no guarantee that your code will work as intended. In particular, if you call that function on the same object from multiple threads then you are at the mercy of data races.

  2. Use an external data structure to store the mutable objects:

    struct A
    {
        static C[const(A)*] _c1s, _c2s;
    
        void compute(ref const(R) r) const
        {
            _c1s[&this] = ...
            _c2s[&this] = ...
        }
    }
    

    I'm using &this as the key to the external hash table, but you would probably be better off using some sort of unique ID. This is a very ugly and tricky solution. I dislike it. Also, note that the hash tables are thread-local, so the same object will have different values on different threads. This may or may not be desirable for your particular application.

  3. Rethink how you use const in D.

    In D, const is transitive and bitwise i.e. logical const is not supported. The purpose of this is to guarantee against concurrent shared data writes. Even though your code may be logically const correct, it will still break if two threads tried to call compute on the same object, so D disallows it and provides no legal escape (no mutable).

    Essentially, you should mark functions as const only when they are bitwise const.

    The result of this is that you should use const a lot less in D than you would in C++ because you require bitwise const a lot less than you require logical const.

    As an example, consider a simple (pointless) generic equal function that tells you if two objects are equal:

    bool equal(T)(T lhs, T rhs) { return lhs == rhs; }
    

    Notice that I haven't marked the function parameters as const. This is on purpose. Testing for equality should not require bitwise const -- it only requires logical const, so enforcing D's level of const on the objects would be needlessly restrictive.

    As jA_cOp says, the D community sees no room for logical const in D, for better or for worse. The problem arises when you try to use D's const like C++'s const. They are not the same, so don't use them in the same way! If there's any possibility at all that a function may require the use of logical const then do not mark them as bitwise const!

Unmoving answered 30/12, 2011 at 11:51 Comment(4)
similar to your equal(), I have made most of my function parameters const and I haven't had any problems with it so far. Is this because const is still broken?Thinner
I think const is mostly working now. Some things in Phobos aren't const correct though (I think) and there's a big issue with post blit constructors, but I think the majority of const is working.Unmoving
So, instead of a having a larger freedom when adding const (in C++), in D it's the contrary; it is more restrictive to make parameters const?Ori
@Daevius: Yes, it is more restrictive, but what that buys you is guarantees. If I have an immutable(T)* then I am guaranteed that the object will never ever change, even in a logical const way. Knowing this, I can safely read it from multiple threads. C++'s const does not give this guarantee. It's similar to static typing vs. dynamic typing: static types are more restrictive by definition, but they protect you against certain kinds of errors.Unmoving
H
6

D's immutable is transitive, when given an immutable reference (such as the this reference in an immutable member function), all fields are immutable too. There is no way around this in D. const only exists in D to bind mutable and immutable data, but since immutable is implicitly convertible to const, it has to be transitive too. Once you go immutable (or const), you can't go back.

The benefits are several: immutable data can be shared across threads safely, can be put in ROM if desirable, and is easy to reason with.

There simply is no room for logical const in D, for better or worse.

Hypertrophy answered 30/12, 2011 at 5:42 Comment(2)
so you are suggesting that I make _c1, _c2, compute() and all references to A non-const?Thinner
@Arlen, it is your only choice if you need to mutate the this object in your method. Note that you can still mutate data stored elsewhere (duh), which you can sometimes leverage when the intention is to use mutable data for cache purposes. But such external caching is ugly in the general case (hash map etc. to accompanying cache? Ew!) and remains an unsolved problem.Hypertrophy
R
4

Short version; you can't, by design.

Longer version, the designers of D concluded (after some epic debates) that the benefits of mutable are outweighed by the downsides. (See: jA_cOp's answer for some of the details, many of the other reaons are driven by a intent to make concurrent programming and the reasoning there of less ugly.)

Recurved answered 30/12, 2011 at 8:8 Comment(0)
U
3

You have three options to deal with this:

  1. Cast away the const. This will shit the compiler up, but there's no guarantee that your code will work as intended. In particular, if you call that function on the same object from multiple threads then you are at the mercy of data races.

  2. Use an external data structure to store the mutable objects:

    struct A
    {
        static C[const(A)*] _c1s, _c2s;
    
        void compute(ref const(R) r) const
        {
            _c1s[&this] = ...
            _c2s[&this] = ...
        }
    }
    

    I'm using &this as the key to the external hash table, but you would probably be better off using some sort of unique ID. This is a very ugly and tricky solution. I dislike it. Also, note that the hash tables are thread-local, so the same object will have different values on different threads. This may or may not be desirable for your particular application.

  3. Rethink how you use const in D.

    In D, const is transitive and bitwise i.e. logical const is not supported. The purpose of this is to guarantee against concurrent shared data writes. Even though your code may be logically const correct, it will still break if two threads tried to call compute on the same object, so D disallows it and provides no legal escape (no mutable).

    Essentially, you should mark functions as const only when they are bitwise const.

    The result of this is that you should use const a lot less in D than you would in C++ because you require bitwise const a lot less than you require logical const.

    As an example, consider a simple (pointless) generic equal function that tells you if two objects are equal:

    bool equal(T)(T lhs, T rhs) { return lhs == rhs; }
    

    Notice that I haven't marked the function parameters as const. This is on purpose. Testing for equality should not require bitwise const -- it only requires logical const, so enforcing D's level of const on the objects would be needlessly restrictive.

    As jA_cOp says, the D community sees no room for logical const in D, for better or for worse. The problem arises when you try to use D's const like C++'s const. They are not the same, so don't use them in the same way! If there's any possibility at all that a function may require the use of logical const then do not mark them as bitwise const!

Unmoving answered 30/12, 2011 at 11:51 Comment(4)
similar to your equal(), I have made most of my function parameters const and I haven't had any problems with it so far. Is this because const is still broken?Thinner
I think const is mostly working now. Some things in Phobos aren't const correct though (I think) and there's a big issue with post blit constructors, but I think the majority of const is working.Unmoving
So, instead of a having a larger freedom when adding const (in C++), in D it's the contrary; it is more restrictive to make parameters const?Ori
@Daevius: Yes, it is more restrictive, but what that buys you is guarantees. If I have an immutable(T)* then I am guaranteed that the object will never ever change, even in a logical const way. Knowing this, I can safely read it from multiple threads. C++'s const does not give this guarantee. It's similar to static typing vs. dynamic typing: static types are more restrictive by definition, but they protect you against certain kinds of errors.Unmoving

© 2022 - 2024 — McMap. All rights reserved.