ISO C++ char warnings

I have a sketch that compiles with IDE version 1.8.9 and runs just fine for the MKR1000.

I want to port the sketch to the Nano RP2040 Connect. I installed the board support and changed the board selection. I now get compile warnings for variable definitions that weren't a problem compiling for the MKR1000:


C:\Users\Ira\Documents\Arduino\My Sketches\Alarm Panel\Alarm_Keypad_RP2040\Alarm_Keypad_RP2040.ino:135:82: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]

   char* KeyText[]   = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"};

My usage of this (and several other similar variables that generate warnings) is:

        char* KeyText[]   = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"};
        char CodeNo[5] = "    ";
.
.
.
        strcpy(CodeNo,"    ");
.
.
.
        CodeNo[0] = CodeNo[1];
        CodeNo[1] = CodeNo[2];
        CodeNo[2] = CodeNo[3];
        CodeNo[3] = '\0';
        strcat(CodeNo, KeyText[y*3+x]);

x and y are coordinates of buttons on a screen - not really relevant to my question.

So what would be the correct way of defining KeyText?

I'm having issues wrapping my head around char, string and String, and based on some forum feedback, I'm trying to avoid using String, apparently due to efficiency issues. Is there a good tutorial out there that explains the intricacies of these data types and conversions?

Thanks.

AFAIK single character is single quote, not double quote.
So:

char* KeyText[]   = {'1', '2', '3', ...

https://en.cppreference.com/w/cpp/language/character_literal

I tried that, I generates and error as opposed to a warning:


Alarm_Keypad_RP2040:135:82: error: invalid conversion from 'char' to 'char*' [-fpermissive]

   char* KeyText[]   = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'};

                                                                                  ^
const char* KeyText[]   = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"};

String literals are read-only, you need to declare KeyText as an array of pointers to const char.

4 Likes

Sorry bout that:

Not sure why you'd want it to be a pointer to a character array in the first place...?

Btw, you can conveniently test small snippets here: https://cpp.sh/
Example:

#include <iostream>

char str[] = {'1', '2', '3'};
int main()
{
  for (uint8_t i = 0; i < 3; i ++) {
      std::cout << str[i] << "\n";
  }
  
}

Unless you do expect to be able to modify what’s pointed. That’s the assumption made by the compiler since OP did not use const

This is an entirely valid, but useless statement

"AB";

It's a string literal: a series of char with an implicit NUL-terminator (subtly different from NULL); three bytes. Because it doesn't "do anything" a compiler might ignore it completely, but it could generate those bytes in the program code: 0x41, 0x42, 0x00. To actually start doing something with it, you can assign it to a variable

auto x = "AB";

What's the type of this variable? With no runtime type information, you could force the compiler to tell you with a deliberate error

auto x = "AB";
float f = x;

That error message is

cannot convert 'const char*' to 'float'

And what is that exactly? The cdecl utility can help

$ cdecl
Type `help' or `?' for help
cdecl> explain const char *x
declare x as pointer to const char

Note the difference with

cdecl> declare x as const pointer to char
char * const x

So what is "const char"? Remove the invalid statement. Create a second pointer with the same string literal, and a third pointer to a different string.

auto x = "AB";
auto y = "AB";
auto z = "JK";
Serial.println(reinterpret_cast<uintptr_t>(x), HEX);
Serial.println(reinterpret_cast<uintptr_t>(y), HEX);
Serial.println(reinterpret_cast<uintptr_t>(z), HEX);

Printing the numeric value of the pointers, the first two are the same. (And the third is three bytes away.) The compiler can see two literals result in the same string, and generates just one copy. Each of the characters in the string is a const char that cannot be changed. This allows sharing; otherwise changing a character would affect all the uses of that string. If you try

y[0] = 'T';

the error is "assignment of read-only location '* y'". You see "const char *" a lot in function declarations. It's a promise, enforced by the compiler, that the function will not change any of the characters through that pointer. Compare that to a writable string buffer: "char *". You can pass either to the const one, because it's not going to change anything anyway. But you can only pass a writable buffer if the function does not declare const, because the function is allowed to modify it. The strcat function you're using exemplifies this

char* strcat( char* dest, const char* src );

The src being added is not modified, but the dest is. Using "a bunch of characters with a zero at the end", and str functions like that is how string operations are done "the old way". It gets less efficient with longer strings because you sometimes have to scan the whole string looking for the NUL-terminator. And if it's not where it's supposed to be for some reason, bad things happen. Or if the buffers you create are not big enough, bad things happen.

To address such issues, C++ has its own std::string; but the Standard Library is not available on all Arduino platforms, so they created their own String class with a Java[Script]-like API. That has its own gotchas, especially on more constrained platforms.

In your usage

if any of the KeyText is more than a single character plus NUL, then strcat will overwrite past the end of CodeNo. You might be tempted to use strncat, but that's actually the wrong variant: you might end up losing the NUL in the buffer. strlcat does not have that problem.

But even better: a string literal is an easier- to-type array of single char

const char KeyText[] = "123456789*0#";

You don't have to put the '\0' so that strcat starts in the right place; you don't even use it. Just overwrite that last character directly. The NUL at the end of the original string of spaces (at index [4]) is never touched

CodeNo[3] = KeyText[y*3+x];

Yes, much cleaner and more efficient. Thanks!

This takes both RAM and flash memory… not sure it’s better if you have no intent whatsoever to modify the characters….

That would be
char KeyText[] = {'1', '2', '3', ...

but he is doing char* which is char pointer.
The name of every array is a pointer to the array variable type.

char *KeyText[ 6 ][ 2 ];
for ( byte i = 0; i < 6; i++ ) {
  KeyText[ i ][ 0 ] = '0' + i + 1;
  KeyText[ i ][ 1 ] = 0;
};
1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.