1

I am compiling some code I didn't write, and it bombs out with the error message

invalid conversion from 'char' to 'const char*' [-fpermissive]

on the line

if (loginPassword == '\0') loginPassword = "";

I don't see why that line is any different than the previous strings, and the answers I can find all have to do with variables that start as char.

What is wrong with this code, and how do I fix it?

    void saveUnit(String tempUnit, String supportMode, String loginPassword, String minTemp, String maxTemp, String tempStep) {
      const size_t capacity = JSON_OBJECT_SIZE(6) + 200;
      DynamicJsonDocument doc(capacity);
      
      // if temp unit is empty, we use default celcius
      if (tempUnit == '\0') tempUnit = "cel";
      doc["unit_tempUnit"]   = tempUnit;
      
      // if minTemp is empty, we use default 16
      if (minTemp == '\0') minTemp = 16;
      doc["min_temp"]   = minTemp;
      
      // if maxTemp is empty, we use default 31
      if (maxTemp == '\0') maxTemp = 31;
      doc["max_temp"]   = maxTemp;
      
      // if tempStep is empty, we use default 1
      if (tempStep == '\0') tempStep = 1;
      doc["temp_step"] = tempStep;
      
      // if support mode is empty, we use default all mode
      if (supportMode == '\0') supportMode = "all";
      doc["support_mode"]   = supportMode;
      
      // if login password is empty, we use empty
      if (loginPassword == '\0') loginPassword = "";
      doc["login_password"]   = loginPassword;
      
      File configFile = SPIFFS.open(unit_conf, "w");
      if (!configFile) {
        // Serial.println(F("Failed to open config file for writing"));
      }
      
      serializeJson(doc, Serial);
      serializeJson(doc, configFile);
      
      configFile.close();
    }
8
  • I'm guessing this was copied out of a context where loginPassword was a pointer rather than String, where it probably should have really been compared with nullptr or 0 depending on the era of C++.
    – timemage
    Jun 13 at 20:35
  • Okay. If that's the original context, I have no idea why it ever compiled. You don't get this same error for supportMode and the others that follow this same pattern?
    – timemage
    Jun 13 at 20:54
  • Correct, this is the first line that breaks. I can't figure why, when it should break before this. Jun 13 at 20:59
  • It seems likely all that follow the pattern if (X == '\0') X = some_default; should have been written if (X == "") ... or maybe more efficiently if (X.length() == 0) ..., but this is sort of nonsense for the loginPassword field. If it's already blank... it's blank.
    – timemage
    Jun 13 at 20:59
2

Regardless of the findings below, you don't really want to compare a string object with a null-character this way (more on that below). Really something like: if (someArduinoStringObj.isEmpty()) { is a better idea, no matter what core you're on.

So for anyone that has landed here with this problem with code that had worked and no longer works, the answer is basically don't do that; probably shouldn't have been doing that in the first place.

If you're curious on what I found, there's material on that below.


So, I don't have a complete handle on this, but I do have something to report. When we started going through this I didn't think there would be anything going on in the Arduino board support packages that would have any effect on this, but it turns out that's not the case.

We worked out that under ESP8266 Arduino core 3.0.0 the lines of the following form fail to compile and under version 2.7.4 the compile without error. Here under 3.0.0 it was not picky about specifically loginPassword.

if (someArduinoStringObj == '\0') someArduinoStringObj = "some literal";

This can be abstracted to the somewhat nonsensical:

someArduinoStringObj == '\0';

Since that's the part makes the difference. I really thought there was something more clever going on in the WString.h/WString.cpp code, an extra constructor or implicit conversion operator call prior to the code for the == operator. There is an explicit constructor for String that takes a single char, but it's just that, it's explicit; it doesn't take part in evaluating the == expression above. And there's no automatic conversion from String to const char *. It turns out that isn't anything clever going on in String class when it comes to this problem. In fact you can boil this down to something similar so that the String class itself isn't actually involved at all, into:

void func(const char *) {}

void setup() {
  func('\0');
}

// ...

This will likewise pass compilation under 2.7.4 and fail with a similar error message under 3.0.0. So, with String it is just trying to call the operator==(const char *), even though it's being given a char type. Before getting into that, I tested a theory, which was to find out what happens if given == '\1'; or == 'X') etc, any non-zero value, because 0 has had an interesting relationship with pointers in C++ and C. It makes more sense once you realize there's special going on in String.h. We're basically talking about func('\0') vs func('\1') or func('X') Well, under 2.7.4 these values fail where '\0' does not, but under 3.0.0 both fail. So I got looking into the platform.txt file on both core versions, thinking maybe I'll spot a difference in compiler options. E.g. it would make "sense" if 2.7.4 had -fpermissive where 3.0.0. didn't, but that's not the case. The major difference is that 3.0.0 has -std=g++17 and 2.7.4 has -std=g++11. Just for the hell of it I stole the command-line out of the verbose output compiling for the .ino.cpp file and replaced -std=g++17 with -std=g++11 and applied it to a cut-down file; didn't make any difference. So, as far I tell there's some difference in default options specified between the two compiler versions in use in the two esp8266 package versions.

Constant expressions that evaluate to zero of int type (at least), will convert to null pointer constants. I haven't been in the habit of using character-typed ('\0') zero valued constant expressions for null pointers, and I haven't dug through the various editions of the C++ standard to see what each one has to say about them; color me unsurprised either way. But in any case, that's is basically what's happening here: The string object is being compared to a null pointer constant, at least when it compiles. And if that makes you go WTF, well, me too. If you follow this out, you ultimately you end up here:

unsigned char String::equals(const char *cstr) const {
    if (len() == 0)
        return (cstr == NULL || *cstr == 0);
    if (cstr == NULL)
        return buffer()[0] == 0;
    return strcmp(buffer(), cstr) == 0;
}

So, roughly speaking someArduinoString == '\0', where it will compile, is equivalent to someArduinoString == nullptr which is sort of the same effect as someArduinoString == "". So, what you're seeing is not exactly an implicit form of someArduinoString == String('\0').

I can't picture wanting to say someArduinoString == '\0' over someArduinoString.isEmpty(), but in terms of why it doesn't work on 3.0.0, I'm still unsure. I can tell you that it will if you plant -fpermissive into platform.txt, but that doesn't really explain it. Sometime when I'm really bored I'll see what the language standard actually says and what versions of g++ did what under what options.

Even I worked that out my answer would still be: don't do that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.