How to replicate the functionality of `strtod`, etc. without getting the warning "Assigning to 'char *' from 'const char *' discards qualifier"?

Viewed 141

There are many posts about this particular warning, but I wasn't able to find one that specifically references a function signature like that of strtod and friends.

I have a function int foo(const char *str, char **end), and there's no way that I have discovered to set *end = str before returning the integer without encountering the aforementioned warning. Is there some additional qualifier that I'm leaving out, or is it something I'm going to have to "live with"?

2 Answers

If I remember correctly, this is actually a big fat wart in the modern C Standard. It's impossible to implement strtod without an explicit cast. There was a huge amount of discussion about this, back in the day, with radical proposals being made for bizarre extensions to the language to make it possible to write strtod "correctly". But the cures were all worse than the disease, so in the end, the radical proposals were not adopted, and the result it that it's tricky (but not impossible) to write strtod and the like.

In your implementation, you will typically have a pointer p that points to the first character you didn't parse. Since p's initial value was your input string, which was const char *, your p will typically be a const char *, too. (And this is fine, because you don't intend to use p to modify the string as you parse it.) But when it comes time to set endp, you're simply going to have to use a cast:

*endp = (char *)p;

It feels lousy to be "casting away constness" like this, but it's really the only way, and it's actually perfectly legal, as Eric P. explains in another answer here.

(Another possibility is *(const char **)endp = p;, but it's more typing, even more sketchy-looking, and it turns out not strictly legal.)

Normally the rule is that explicit casts like this are poor form. Normally the recommendation is to find a way to not need the explicit cast. And although the general rule is a good one, this is an exception, pure and simple. Based on everything else that's going on, the conclusion is that you need this cast here, and if you try to get by without it, you end up having to do something even worse elsewhere.

In answer to a question in a comment, the reason we can't make endp be a const char ** is that it makes things too inconvenient on the caller. The caller might be using pointers that are not const-qualified. Now, if the caller has

char *str = "123.456xyz";

and then calls

double d = strtod(str, NULL);

this is fine: it's okay to pass a regular char * to a function that expects const char *. But if the caller wants to get the end pointer back, and additionally declares

char *endp;

and then calls

double d = strtod(str, &endp);

and if strtod were declared as

double strtod(const char *, const char **);

it turns out it wouldn't work. You can pass a char * to a function that expects a const char *, but you can not pass a char ** to an function that expects a const char **. The explanation for why you can't is rather obscure. There's a sort-of-coherent explanation in the C FAQ list.

… there's no way that I have discovered to set *end = str…

Simply use *end = (char *) str;. When you explicitly convert using a cast, the compiler will not warn you. Further, this is fully defined by the C standard.

C 2018 6.3.2.3 7 says a pointer to an object type (here str) may be converted to a pointer to a different object type (here char *). If the resulting pointer were not correctly aligned for the new type, the behavior would be undefined. However, any pointer is correctly aligned for char, so that is fine. That paragraph also tells us that when we convert a pointer to a character type, the result points to the lowest addressed byte of the object. And that is just what we have done, convert to a pointer to a character type.

These rules do not prohibit us from removing const in the cast: Converting from the const char * that is str to char * is allowed. (A different rule says the behavior is undefined if an attempted is made to modify an object defined with const, but we are not going to do that. Just pointing to it without const is fine.)

Then the caller gets this pointer back in their end object. It is a valid pointer to the byte that ended the parse. So they can use it. If they are working with a string defined without const, they can use this end pointer to read and/or write to the string. If they are working with a string defined with const, they can use this end pointer to read from the string, and they are also free to convert it to a const char *.

Related