Why does returning a pointer to non-const char from a function as pointer to const char in C results in "incompatible return types"?

128 views Asked by At

In C, I try to implement a dynamic array of strings (much like the C++ std::vector<std::string>) with inspiration from https://stackoverflow.com/a/3536261/2690527.

Currently my code looks like

string_array.h:

#ifndef _STRING_ARRAY_H_
#define _STRING_ARRAY_H_

struct string_array_t;

struct string_array_t* create_string_array( size_t capacity );

void free_string_array( struct string_array_t* array );

char const * const * get_string_array( struct string_array_t const * array );

#endif

string_array.c:

#include <stdlib.h>
#include <stddef.h>
#include <string.h>

#include "string_array.h"

struct string_array_t {
    size_t capacity;
    size_t size;
    char** values;
};

struct string_array_t* create_string_array( size_t capacity ) {
    // ... left out ...
}

void free_string_array( struct string_array_t* array ) {
    // ... left out ...
}

char const * const * get_string_array( struct string_array_t const * array ) {
    return array->values;
}

The function get_string_array generates a compiler error. I want that function to provide the caller with direct access to the underlying array, but prevent that the caller can (accidentally) mess with the internal data structure.

Hence, I want to return what I thought is (read from right-to-left) a pointer to (the beginning of an array of) constant pointers to constant chars.

The error message is:

Returning char **const from a function with result type const char * const * discards qualifier in nested pointer types

Why do I get that message? IMHO, this should be OK, because I am not discarding any const but adding more const to it.

3

There are 3 answers

1
Mark Ransom On

Because char and const char are two different types. Even though the const version can be easily used by the non-const version, pointers to the types are considered to be incompatible. If they weren't you could easily cast away constness without even realizing it.

11
John Bollinger On

TL;DR: a "qualified version" of a type refers only to that type itself, not to other types in its derivation.


Why does returning a pointer to non-const char from a function as pointer to const char in C results in "incompatible return types"?

It doesn't. That would be this:

const char *f(size_t s) {
    char *cp = malloc(s);
    return cp;
}

The controlling rule is:

If a return statement with an expression is executed, the value of the expression is returned to the caller as the value of the function call expression. If the expression has a type different from the return type of the function in which it appears, the value is converted as if by assignment to an object having the return type of the function.

(C17 6.8.6.4/3)

const char * is a different type from char *, so the rules for conversion during an assignment apply, and in particular, this constraint applies:

One of the following shall hold:

[...]

  • the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left has all the qualifiers of the type pointed to by the right

(C17 6.5.16.1/1)

(The only other option for pointer assignment involves pointers to void, which does not apply here.)

In the case of a function return, the return value seen by the caller plays the role of the left operand, and the expression in the return statement plays the role of the right operand.

  • The type of the left-hand operand is thus const char *, an unqualified pointer type.
  • The type of the right operand is char *.
  • Pointed-to types, as if subjected to lvalue conversion, are both char, so not only compatible but the same, and
  • const char has all of the qualifiers of char.

All good.


Your code presents a different case. Consider this analog:

char const * const *g(void) {
    char **cpp = NULL;
    return cpp;
}

The same provisions discussed above apply here, too, but they play out differently.

  • The type of the "left-hand" operand is char const * const *, again an unqualified pointer type.

  • The type of the "right-hand" operand is char **.

  • The pointed-to type on the left, as if subjected to lvalue conversion, is char const *

  • The pointed-to type on the right is char *.

  • These are both unqualified types, and they are not compatible (which in this case means "the same"), so the constraint in paragraph 6.5.16.1/1 is not satisfied.

    Qualified versions of char * include char * volatile or char * const -- the overall type is qualified, and the pointed-to type is the same.

A language constraint having been violated, the spec obligates conforming compilers to emit a diagnostic, which is exactly what you observe.


If you want to provide a char const * const * version of your type's internal char ** member then you can cast:

char const * const * get_string_array( struct string_array_t const * array ) {
    return (char const * const *) array->values;
}

However, dereferencing that pointer violates the strict aliasing rule, for much the same reason that you need the cast in the first place. As such, you should probably take care about aliasing-based optimizations your compiler might otherwise perform, too. With GCC, adding -fno-strict-aliasing to your compile command would probably head off any ill effect, but formally, the behavior of such dereferences is undefined.

5
M.M On

This is a flaw in the language design ; the implicit conversion to add const at all levels is safe and should be legal.

It was overlooked originally, and apparently has not been fixed in C23, although this similar case has been fixed.

Instead we are forced to use an ugly cast:

char const * const * get_string_array( struct string_array_t const * array ) 
{
    return (char const * const *)array->values;
}