Using C++'s const keyword to Power CONST

CONST and MUTABLE have been around for a while now, and I think the chosen balance has worked out rather well.

One historical problem point with these mutability features is that there was no compile-time checks to make sure code wasn't violating it. There were tons of cases of PROTECT bits not being honored, simply because there wasn't a check for mutability in some routine. The person hacking on the C code to REVERSE or SORT a series would have to explicitly remember to think that was a mutating operation and check the bit.

The obvious-sounding way to stop these problems from creeping in would be to leverage the const annotation in C and C++. All the routines that modified series would require the caller to have a non-const pointer in their hand...while routines that could be done on read-only series could take either a const or non-const pointer.

So consider the simple example of getting an element at a position in an array:

 Element* Array_At(Array* array, Index n)
     { ...lots of code... }

Historically this would take in a mutable Array (the only kind there was) and give back a mutable Cell. But what we want is for mutable arrays to give back mutable cells, and const arrays to give const cells. So we could simply create a wrapper that calls into the mutable implementation but reskins the result as const for const input:

 Element* Array_At(Array* array, Index n)
     { ...lots of code... }

 inline const Element* Array_At(const Array* array, Index n)
     { return Array_At(m_cast(Array*, array), n); }

There's just one problem... C doesn't support overloading. You can't have two functions with the same name and different signatures and have the compiler pick between them. There'd have to be two different names:

 Element* mutable_Array_At(Array* array, Index n)
   { ...lots of code... }

 inline const Element* Array_At(const Array* array, Index n)
   { return mutable_Array_At(m_cast(Array*, array), n); }

This might not seem like that big a deal, but the combinatorics add up. Because now you can't write a generic macro that speaks about array positions...you have to have macros with different names that call the differently named accessors. And consider there are lots of these routines (Array_Head, Array_Tail, Array_Last... Binary_Head, Binary_Tail... Flex_Data, etc. etc. etc.) It's pretty horrific when you start having this explode with mutable_XXX variations and mutable_XXX variations of everything that calls them.

I came up with a trick to get around it. Basically, the trick is to sacrifice some amount of const checking in C. First, define a macro for something that resolves to const in C but vaporizes in C++:

#ifdef __cplusplus
    #define const_if_c
#else
    #define const_if_c const
#endif

Then, define the functions like this:

Element* Array_At(const_if_c Array* array, Index n)
  { ...lots of code... }

#ifdef __cplusplus
    inline const Element* Array_At(const Array* array, Index n)
         { return Array_At(m_cast(Array*, array), n); }
#endif

So the C build will give you back a mutable array no matter whether your input array was const or not. But the C++ build only gives back const arrays for const input.

This makes systemic enforcement of mutability checking practical. If you're inside the implementation with a const array, string, or binary... you won't be able to make a call to a C routine that will mutate it. The only way you can get mutable arrays is through specific entry points that extract the array with a runtime check to make sure it's mutable.

It's all in the implementation guts...so it only affects those using the core API, not libRebol. The only thing you need to do is make sure you at some point build the code with a C++ compiler, and it will tell you where any problems are.

2 Likes

I've built an even more automatic version of this, that's working very well!

//=//// CONST PROPAGATION TOOLS ///////////////////////////////////////////=//
//
// C lacks overloading, which means that having one version of code for const
// input and another for non-const input requires two entirely different names
// for the function variations.  That can wind up seeming noisier than is
// worth it for a compile-time check.
//
//    const Member* Get_Member_Const(const Object* ptr) { ... }
//
//    Member* Get_Member(Object *ptr) { ... }
//
// Needful provides a way to use a single name and avoid code duplication.
// It's a little tricky, but looks like this:
//
//     MUTABLE_IF_C(Member*) Get_Member(CONST_IF_C(Object*) ptr_) {
//         CONSTABLE(Object*) ptr = m_cast(Object*, ptr_);
//         ...
//     }
//
// As the macro names suggest, the C build will behave in such a way that
// the input argument will always appear to be const, and the output argument
// will always appear to be mutable.  So it will compile permissively with
// no const const checking in the C build.. BUT the C++ build synchronizes
// the constness of the input and output arguments (though you have to use
// a proxy variable in the body if you want mutable access).
//
// 1. If writing a simple wrapper whose only purpose is to pipe const-correct
//    output results from the input's constness, a trick is to use `c_cast()`
//    which is a "const-preserving cast".
//
//    #define Get_Member_As_Foo(ptr)  c_cast(Foo*, Get_Member(ptr))
//
// 2. The C++ version of MUTABLE_IF_C() actually spits out a `template<>`
//    prelude.  If we didn't offer a "hook" to that, then if you wrote:
//
//        INLINE MUTABLE_IF_C(Type) Some_Func(...) {...}
//
//    You would get:
//
//        INLINE template<...> Some_Func(...) {...}
//
//    Since that would error, provide a generalized mechanism for optionally
//    slipping decorators before the template<> definition.
//

In C, It's Nearly A No-Op...

#define CONST_IF_C(param_type) \
    const param_type  // Note: use c_cast() macros instead, if you can [1]

#define MUTABLE_IF_C(return_type, ...) \
    __VA_ARGS__ return_type  // __VA_ARGS__ needed for INLINE etc. [2]

#define CONSTABLE(param_type)  param_type  // use m_cast() on assignment

In C++, It's Some Magic, But Good Magic!

#define MUTABLE_IF_C(ReturnType, ...) \
    template<typename T> \
    __VA_ARGS__ needful_mirror_const(T, ReturnType)

#define CONST_IF_C(ParamType) /* !!! static_assert ParamType somehow? */ \
    T&&  // universal reference to arg

#define CONSTABLE(ParamType) \
    needful_mirror_const(T, ParamType)

A Little Weird, But It Powers The Const Features!

Without something like this, we really couldn't provide a guarantee that your constness was being honored, it's just too brittle...

But with this, it's nigh foolproof!

I'm still thinking about the naming, but I think I want it to be all-caps to call attention to the macro magic.

1 Like