If you want a value to be a true constant, you can pass it as a template value argument.
template<int i>
void function(){
*side_effect = 123;
}
there is no way for any operation to modify i.
This requires that the input be a compile-time constant (and verified so by the compiler at compile time).
So this doesn't work:
int main(){
int i = 0;
side_effect = &i;
function<i>();
}
as i is not a compile-time constant. If we instead made it:
int main(){
const int i = 0;
side_effect = &i;
function<i>();
}
the function<i> line works, but the side_effect = &i doesn't work. If we add in a cast:
int main(){
const int i = 0;
side_effect = const_cast<int*>(&i);
function<i>();
}
now the operation *side_effect = 123 becomes UB within function.
Another approach which doesn't require that the value being passed in is a true compile-time constant is to not take a reference:
void function(int i){
*side_effect = 123;
}
and instead take a local copy (either int or const int as the argument, depending on if we want function to have the rights to modify i).
The full strength version of what you want -- that we take a reference to external data, and then ensure that the external data remains unchanged -- can pretty easily be shown to be equivalent to the halting problem in the general case.