Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confused when I should and shouldn't use "const" in C

I have a dictionary that goes like this:

typedef struct dictNode {
    int key;
    char *value;
    struct dictNode *next;
} Dict;

And a get() function that goes like this:

char *get(const Dict *dict, int key) {
    if(!dict) return NULL;

    Dict *currPtr = dict;

    while(currPtr) {
        if(currPtr->key == key) {
            return currPtr->value;
        }

        currPtr = currPtr->next;
    }
}

Compiling this code produces the following error:
dict.c:85: warning: initialization discards qualifiers from pointer target type

This warning refers to the line:

Dict *currPtr = dict;

If I add a "const" before that line like this:

const Dict *currPtr = dict;

The warning goes away...

1) First thing I don't understand is this: I added "const" to the dict argument in get() so that the compiler warns me if I try to change the address dict is pointing to, otherwise I won't be able to access the dictionary in the main program cause I lost the address I was pointing too. Now, I'm creating a new pointer, currPtr, that points to the same place as dict. This way, I use this pointer instead, to traverse the dictionary and keep dict pointer intact. Why do I also need to have const for currPtr?

2) Second thing I don't understand is this: The line currPtr = currPtr->next; is changing the currPtr pointer, so, why doesn't the compile warn me about that if I added a const to currPtr?

Then I have a del() function that goes like this:

Dict *del(const Dict *dict, int key) {
    if(!dict) return NULL;

    Dict *currPtr = dict;
    Dict *prevPtr = dict;

    while(currPtr) {
        if(currPtr->key == key) {
            prevPtr->next = currPtr->next;
            free(currPtr);
        }

        prevPtr = currPtr;
        currPtr = currPtr->next;
    }

    return dict;
}

Please note that I'm aware that this delete function code is not complete, it does not correctly work if I want to delete the first element for instance. It doesn't matter, I'll finish later, it suffices to demonstrate my problem...

3) So, in the get() function I needed to add const to currPtr but in this del() function, I don't need to add const to currPtr nor prevPtr? In both functions I am changing the currPtr pointer and in the case of the del() function I'm changing the prevPtr pointer too. Why does the get() function require me to add a const before currPtr and the del() function does not require me to add a const before currPtr and prevPtr?

I can basically resume this whole post to: When and where exactly in my get() and del() functions should I use const and why, and, when and where I shouldn't?

like image 413
rfgamaral Avatar asked Apr 02 '09 17:04

rfgamaral


2 Answers

Without a pointer, you would have

const Dict currPtr

which is a constant Dict. Now, if you make it a pointer you have

const Dict *currPtr

which is a pointer to a constant Dict. That does not mean that the pointer is constant. But it does mean that the Dict pointed to is treated as constant

currPtr->key = 10; // error, dict is treated as constant. 

But the pointer is not

currPtr = otherPtr; // possible: the pointer is not constant

You would get an error for the second case if you make the pointer constant. Keeping the pointed dict constant too, this would look like this

const Dict * const currPtr = init;

Now you can't set currPtr to point to something different, because the pointer is now constant, not just what the pointer points to is treated so. Some people like the look if the const is always right to the stuff that it makes const. This would look like this

Dict const * const currPtr = init;

which is the same as the previous snippet. If you then read it from right to left, it tells you what it is "const pointer to a const Dict". If you have a type, it doesn't matter how you order the specifiers

int const a = 10;
const int b = 10;

Both are constant integers. That is why we could put const right of the Dict type specifier.

Now, if you have a pointer, you can always pretend you point to a constant object, even though the object wasn't declared const. But you can't pretend to work with a non-const object if what you point to is a const object:

int const *p = NULL;
// doesn't work without a cast. Will at least provoke a warning
int *pn = p;

int *p = NULL;
// always works: pretending to point to something const doesn't harm.
int const *pc = p;

Note that if you make the pointer itself const, the rules are different to that. They are analogous to const applied to other types:

int const i = 0;
int j = i; // works. we only read the value of i. its const doesn't matter. 

int * const p = NULL;
int * q = p; // works: we only read the value of p (a null pointer).

After copying the value into a new variable (whether pointer or not), the new variable is not connected in any way to the other variable, because the value read has no associativity to how the value was created in the first place. The const of the other variable doesn't matter.

like image 166
Johannes Schaub - litb Avatar answered Sep 25 '22 00:09

Johannes Schaub - litb


"I added "const" to the dict argument in get() so that the compiler warns me if I try to change the address dict is pointing to"

In that case you meant Dict *const dict, not const Dict *dict. You've declared the struct as const, not the pointer to it.

"Why do I also need to have const for currPtr"

Because otherwise you could use currPtr to modify the dictionary struct itself, which is supposed to be const in this function.

"The line currPtr = currPtr->next; is changing the currPtr pointer, so, why doesn't the compile warn me about that if I added a const to currPtr"

Same reason: it's not an error if the const is in the place you put it. It would be if the const were in the other place for currPtr.

otherwise I won't be able to access the dictionary in the main program cause I lost the address I was pointing too

No - when the main program calls get, it passes a pointer value into the routine, and this becomes the value of dict in the get function. But get has its own pointer variable, separate from the one in the main program. Even if they just so happen to both be called "dict", changing one pointer won't change the other.

Both variables point to the same struct, so if you use the variable to modify any of the fields in the struct, then of course the results will affect both bits of code. Usually, a function called "get" would be a read-only operation, so you're right to make the parameter const Dict *. But I think the reason this is right, is slightly different from what you think. It's to protect the contents of the dictionary from change, not to protect your record of the address of it.

However, const-safety is slightly awkward for linked lists like this one. Even when you have a const Dict, its "next" field still doesn't point to const. So although you can't modify the first node, the const-system won't protect you from modifying other nodes in the list. C++ addresses this with function overloading, so you can have a "getnext" function which returns a const output if the input is const, but a non-const output if the input is non-const. You could achieve a similar thing in C with a "getnext" and "getnext_c", the latter having a const parameter and return. But most people don't bother, including standard library functions like strchr, which in C takes const char* but returns non-const char*. strchr can therefore be accidentally used in C to convert a const string into a non-const one, without any warning. Or deliberately, in which case it's a bit like laundering money through a "legit" business ;-)

the del() function does not require me to add a const before currPtr and prevPtr?

I confess I'm stumped on that. I would expect the same warning as in "get" - are you sure it isn't there? But the parameter to del shouldn't be const anyway, because you're potentially modifying the fields.

like image 30
Steve Jessop Avatar answered Sep 22 '22 00:09

Steve Jessop