0

I have noticed that constness on a typedef'ed pointer loses its ability to be converted implicitly to a const type of the un-typedef'ed type.

Since I am clearly lacking proper vocab to explain the issue, I'll show some examples.

The following code fails to compile with GCC 8.3.0

typedef char* char_ptr;
void myFunc(const char_ptr param);
void test() {
  const char str[] = "Hello, World!";
  myFunc(str);
}

It gives the following error:

error: invalid conversion from ‘const char*’ to ‘char_ptr’ {aka ‘char*’} [-fpermissive] myFunc(str);

I'm not looking for a workaround. I know many ways to solve this, such as casting the type or changing the function declaration.

I'd like to understand why I get this error, and why it only happens with const of a typedef'ed pointer type.

I have narrowed down some use-cases to try to understand when the compiler thinks that types are identical https://ideone.com/Rk9gD9

typedef char char_t; // Char type
typedef char* char_ptr; // Char pointer
typedef const char* char_const_ptr; // Char const pointer

IS_SAME( char, char );                  // Obviously true
IS_SAME( char, float );                 // Obviously false

// Testing char_t
IS_SAME( char, char_t );                // true: OK
IS_SAME( const char, char_t );          // false: OK
IS_SAME( char, const char_t );          // false: OK
IS_SAME( const char, const char_t );    // true: OK
    
// Testing char_ptr
IS_SAME( char*, char_ptr );             // true: OK
IS_SAME( const char*, char_ptr );       // false: OK
IS_SAME( char*, const char_ptr );       // false: OK
IS_SAME( const char*, const char_ptr ); // false: Why?
IS_SAME( char* const, char_ptr );       // false: OK
IS_SAME( char* const, const char_ptr ); // true: Why?

// Testing char_const_ptr
IS_SAME( char*, char_const_ptr );       // false: OK
IS_SAME( const char*, char_const_ptr ); // true: OK
IS_SAME( char* const, char_const_ptr ); // false: OK

I can correctly predict all cases, except the ones with is_same<const char*, const char_ptr> and is_same<char* const, const char_ptr> which are the opposite of what I expected them to be.

My understanding from this SO topic is that writing const before a typedef'ed pointer type is actually equivalent to writing const after the un-typedef'ed pointer type (which matches results from the above test). I just can't seem to relate to a good memory technique for reminding myself in the future when the const type qualifier should be written before or after the typedef'ed or untypedef'ed type.

vdavid
  • 2,434
  • 1
  • 14
  • 15
  • 3
    `const char_ptr` means `char* const`, not `const char*`. You are making the _pointer_ const. – Drew Dormann Apr 04 '23 at 18:49
  • 1
    Clang gives a much better diagnostic: https://godbolt.org/z/qWhKTsv6v – user4581301 Apr 04 '23 at 18:51
  • 2
    Side note: In modern C++, prefer `using` over `typedef`. The former can do everything the latter can - and more. There's no reason these days to ever use `typedef`. – Jesper Juhl Apr 04 '23 at 18:51
  • This is why I prefer using the right-to-left rule – Den-Jason Apr 04 '23 at 18:52
  • This is a good reason to practice **east const**. `char_ptr const` means `char* const`. – Drew Dormann Apr 04 '23 at 18:53
  • This is a good answer to a very similar question: https://stackoverflow.com/a/55461574/1607937 – Den-Jason Apr 04 '23 at 18:55
  • `typedef`s/`using`s sort of bracket what they `typedef`, like as if you had put `typeof(...)` around it. So `const char_ptr` == `const typeof(char *) ` == `typeof(char*) const` , where where the last doesn't need the typedef bracketing and so can be just `char *const`. Don't know if it makes it clearer. – Petr Skocik Apr 04 '23 at 18:56
  • Basically with the `typeof` language feature you could replace `typedef`s with defines (`#define your_target_name typeof(what_you_typedef)`) if you didn't mind the loss of scoping. Without it, a plain define of the `#define char_ptr char *` kind would be opened to being turned into `const char*` via a preceding `const`, but typedefs behave as if it were `#define char_ptr typeof(char*)` and scoped. – Petr Skocik Apr 04 '23 at 19:02

3 Answers3

1

I'll illustrate what's happening with the typedef and the added const with this code (that passes the static assertion):

#include <type_traits>

typedef char* char_ptr;

static_assert(std::is_same_v<const char_ptr, char* const>);
// which is the same as
static_assert(std::is_same_v<char_ptr const, char* const>);

const applies to the pointer - not the char it's pointing at. const always applies to what's left of it - unless there's nothing to the left, then it applies to what's right of it, but it's applied to the whole char_ptr as-if by doing char_ptr const.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
1

This typedef declaration:

typedef char* char_ptr;

declares the name char_ptr as an alias for the type char *. That is, as an alias for pointer to non-constant char.

So, this function declaration:

void myFunc(const char_ptr param);

declares its parameter param as a constant pointer to non-constant char.

It may be rewritten like:

void myFunc( char * const param);

Pay attention to that. To determine the type of the function, the high level qualifier conts is discarded. That is, the above function declaration is equivalent to the following declaration (that is, not a function definition):

void myFunc( char * param);

For example, you may declare the function like:

void myFunc( char * param);

and define it like, for example:

void myFunc( char * const param)
{
    std::cout << param << '\n';
}

Or vice versa, you may declare the function like:

void myFunc( char * const param);

and define it like:

void myFunc( char * param)
{
    std::cout << param << '\n';
}

In the first function definition, you may not change the pointer itself, like for example:

std::cout << ++param << '\n';

But, in both function definitions, you may change the character pointed to by the pointer, for example:

*param = 'A';

because the pointer, independent of whether it is itself a constant pointer or not, is a pointer to a non-constant character.

On the other hand, in the function test(), you declared an array of constant character:

const char str[] = "Hello, World!";

Using the array as an argument expression, it is implicitly converted to a pointer to its first element, of type const char *.

And, you are trying to pass this pointer of type const char * to the function, which the corresponding parameter has the type char * const. That is, the argument expression has type pointer to constant object, while the parameter has type constant pointer to non-constant object.

So, the compiler issues an error.

You could declare the typedef like:

typedef const char* char_ptr;

then the function declaration:

void myFunc(const char_ptr param);

will be equivalent to:

void myFunc(const char * const param);

and you could call the function from the function test() like:

void test() {
    const char str[] = "Hello, World!";
    myFunc(str);
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
1

You have to remember that typedef is not a marcro.

Your interpretation seems to think you can replace the typedef (the alias) textually into the code and it is the same. This is not true. It creates a new type alias (think of it like adding brackets around the type).

typedef char char_t; // Char type
typedef char* char_ptr; // Char pointer
typedef const char* char_const_ptr; // Char const pointer

The other rule to remember is that const binds left. Unless it is on the very left then it binds right. So moving the const from the "very left" one place to the right gets you the same type.

I always move the const away from the "very left" as it makes reading the type easier (types read right to left). To do consistent comparisons, you should do this as you now have a standard location for const and you can do the textual comparison.

So I would look at your types as:

char_t         =>  (char)
char_ptr       =>  (char*)
char_const_ptr =>  (const char*)   => (char const *)

Now looking at your questions:

IS_SAME( const char*, const char_ptr ); // false: Why?
IS_SAME( char* const, const char_ptr ); // true: Why?

const char*         =>                                          char const *
char* const         =>                                          char * const


const char_ptr      => const (char*)    => (char*) const     => char * const

See Reading Types

Martin York
  • 257,169
  • 86
  • 333
  • 562
  • "`const` binds left. Unless it is on the very left then it binds right" is a very interesting way of looking at it. Thank you. It also makes me wonder why the C standard did allow such confusing notation. – vdavid Apr 05 '23 at 09:24