Prevent function from changing arguments (like constant pointer in C)?


#1

In C, one can declare that an input for a function is a constant pointer. The function can read from it, but not write to it. Is there an equivalent in Julia?

Context: I’m writing a function, foo, that takes in a user’s function, bar. foo calls bar with some arguments, and I want to make sure bar isn’t modifying the arguments that foo is giving it. Those arguments can consist of mutable composite types of arrays and such things that might be easily overwritten.

I could create a deep copy before passing it in, but this seems like extra work – these arguments might become very large.

Here’s the pattern:

function foo(bar)
    ins = go_get_a_bunch_of_structured_data()
    outs = bar(ins) # Could use deepcopy, but that takes a long time.
    something_else(ins) # Hopefully not changed by 'bar'!
end

#2

As a matter of fact, you can’t do that in C. Casting away the const is NOT a UB in C (it is a UB if you want to modify an object with const storage I believe). So keeping that in mind

Yes, don’t put ! in the function name and document the expected behavior.


#3

The only thing I can think of is to take something like an array and wrap it in a read-only view type (i.e. an AbstractArray type much like SubArray but which doesn’t define setindex!).


#4

You could test the user function on small set of arguments and error if they get modified


#5

I’m probably misunderstanding what you’re saying, but what I mean to say is that the following compiles fine in C++:

int foo(double * x)
{
  x[1] = 3.14;
  return int(x[1]);
}

whereas this (note the const) does not compile (g++ 4.7):

int foo(const double * x)
{
  x[1] = 3.14;
  return int(x[1]);
}

The error it throws is error: assignment of read-only location.

I was hoping to take advantage of a similar pattern in Julia.

The reason I’m so concerned with this is that I’m writing part of a package, and the particular thing I’m doing here is something users (who haven’t read every last little note in the documentation) mess up with alarming regularity. I’m trying to design a good interface so my users will know that they’ve created a problem without undue run-time overhead.

Thanks, @bramtayl. This would probably be good most of the time, but if there’s in if in there, I’ll never know if some branches change the inputs and some don’t. You’re right that it’s better than nothing!

That’s an interesting idea.


#6

The const here is only a hint and has no effect on the program’s behavior (ok it affects overload but that’s it)

The user function can simply do double *_x = (double*)x; or double *_x = const_cast<double*>(x); and the code is perfectly well defined C/C++ code.


#7

Thanks, @yuyichao. I see your point. I think a hint is totally acceptable for me (especially one that throws a compiler error!). I’m just trying to prevent honest accidents. So I take this to mean that no such hint is possible in Julia?


#8

Certain protections are possible, eg the one mentioned by @stevengj above, but at the same time they can be circumvented by a determined user (eg if wrapping in a struct that does not define setindex!, simply access the relevant field directly). You might as well just follow good practices (functions that modify end in !).

For small vectors, also see


#9

Ok, thanks all. I’ll probably use a deepcopy to prevent the user from being able to mess up the data.

Does anyone out there think it would be useful to have some kind of hint to Julia that a given argument should not be modified? I don’t care if it’s bullet-proof – just something to prevent slip-ups. Does this even make sense for the way Julia deals with the code?


#10

The main problem I have with this is that the promise of non-mutability propagates to all functions you call, so for example if you call length on an array, that function must be annotated as non-mutating. Having spent countless hours of chasing const-correctness in C++, I can’t help but wonder about the following:

  1. How many programming errors were actually prevented?
  2. Would it not have been simpler to just fix any errors like this, rather than chasing the holy grail of const correctness only to end up being forced to do a const_cast because of some external library?

#11

Let’s say Julia had a special type corresponding to constant pointers. Let’s call it Immutable. Calling length(Immutable(A)) where a is an Array would then require that length has a method for Immutable{Array}.

Unlike C, the library that provides the length function would not be machine code. Even if the programmer who wrote the length function didn’t foresee an input of this type, the JIT compiler will attempt to create a method for it. Given proper sub-typing rules, it should succeed, but the new method will throw an error whenever setfield! tries to change a field in the input, and getfield will convert mutable fields into Immutable. So the task of “chasing const correctness” would basically be handled by the compiler and the dispatch system. (It would even be possible to call functions that sometimes mutate their input, and the call would succeed if they did not mutate in this particular case.)

It should still be possible to get a Ptr from an Immutable using Base.unsafe_convert, so that ccall will work as expected. Thus unsafe_convert could also be allowed to convert Immutable{T} into T directly. This would make it clear that Immutable objects are only for detecting programmer errors. It would be much, much harder to implement this as a security feature.

(Overwriting setfield! and getfield for a user type is not allowed, for obvious reasons. So this would have to be implemented at a lower level.)


#12

This looks like an elegant solution to me, especially since this is completely opt-in. It reminds me of a feature I have wished for:

struct Foo{T} <: T end
# or
mutable struct Foo{T} <: T end

The idea is that this would create a wrapper type that behaves exactly like T, except that it is possible to define methods that specifically take a Foo{T}. It would not be allowed to add any fields, only the fields already in T would be present. This combined with getfield/setfield overriding would allow implementing your Immutable as a special case. Another application could be logging of calls to specific functions. My use case for it would be to make C++ smart pointers behave the same way as the pointed-to type.

Since this effectively introduces inheritance from a concrete type in this specific case, I’m pretty sure there are some major objections to implementing this.