What happens when a function's arguments are function calls?

Hi, Arduino fans
FYI, Just started with C++ and Arduino+ESP32 last month, experienced in C#,C,VB etc.

The problem: I run into what looks as strange behavior to me, trying to assemble a date string in the US format (and later on in different Europian formats like Dutch) as : DayOfWeek, Month Day YYYY.
The code below is condensed to the relevant part, using TimeLib.h for the called time functions like dayStr().
The sketch, originally developed/running under Arduino IDE, now under VS2019-vMicro.
The problem:
The std::string returned (today) is : "July, July 27 2021"
Where it should read: "Tuesday, July 27 2021"
If I print out the result of dayStr(weekday(timenow)) separately it is: Tuesday
My question:
Why is the result of monthStr(month(timenow)) also assigned to the day part in sprintf? And how to avoid this ofcourse. Use separate vars for each argument?

std::string FormatTimeString(time_t timenow, bool fulldate, bool onlydate = false, bool secsinclude = false) {
    const int sizBuf = 100;
    char spart[sizBuf];
    int n;
    std::string fstring = "";

    // Optional
    if (fulldate || onlydate)
    {
        n = sprintf(spart, "%s, %s %d %d", dayStr(weekday(timenow)), monthStr(month(timenow)), day(timenow), year(timenow));
        Serial.println(spart);
        Serial.println(dayStr(weekday(timenow)));
        if (n > 0) {}
        else {
            perror("Error:");
        }
        //Serial.printf("Date string is %d characters long: %s \n", n, spart);
        fstring = std::string(spart);
        if (onlydate) { return fstring; }
    }
    // Additional time fields…
}

The Time library keeps the strings in PROGMEM so it can't return a pointer to the string directly. It also can't return a pointer to a local variable since that variable will go out-of-scope when the function returns and the memory may be re-used.

Their solution was to have a single buffer to copy the string into. All of the functions that look up strings use the some buffer.

Copy the strings into local variables before you use them.

Thanks for this John.
The thing is, I had the time parts in different vars originally but this looked a bit silly (to me) in terms of compact programming, so I switched to the all-inline solution.
Reuse of a buffer was indeed what I thought to see.
I assumed (wrongly) that each argument of sprintf - the function call(s) - were executed one by one, result stored in each own temp location, and then passed on to the executing sprintf. I assumed again wrongly that storing of the intermediate results is a task of the executing sprintf - argument parsing.
This looks valid for base types like int but not for string pointers.
Correct me if I'm wrong.
So separate vars(buffers) for the strings.

Johan

that was a correct assumption.
...but what you get back from dayStr() is a pointer to a place in memory that is reused by the next call monthStr() so sprintf() gets actually twice the same pointer and when it goes read the cString to render the final result, of course same pointer = same content twice.

using strncat() and building your spart buffer in chucks should work

(alternatively you could build that up directly into fstring using std::string methods)

@johnwasser and @J-M-L

A further search in the forums took me 6 years back...
See I was not the only one

Still informational today!

Johan

As a closing remark see below a working version, serial output shown first. Thanks for the advice on my first posting. The project is a Weather Station( :+1:Rui Santos), showing temp, humidity, time, date.
The String function takes care of copying the buffer content via pointer (char *) to a new mem location.

Johan

Serial output
23.80 -> temperature
64.50 -> humidity
09:36 -> time
Date string is 23 characters long: Wednesday, July 28 2021

Code

std::string FormatTimeString(time_t timenow, bool fulldate, bool onlydate = false, bool secsinclude = false) {
    int n;
    std::string fstring = "";

    // Optional
    if (fulldate || onlydate)
    {
        String dpart = String(dayStr(weekday(timenow)));
        String mpart = String(monthStr(month(timenow)));
        n = sprintf(spart, "%s, %s %d %d", dpart, mpart, day(timenow), year(timenow));
        Serial.printf("Date string is %d characters long: %s \n", n, spart);
        fstring = std::string(spart);
        if (onlydate) { return fstring; }
    }
    // Additional time fields…
}

not sure sprintf knows how to deal with String. probably

n = sprintf(spart, "%s, %s %d %d", dpart.c_str(), mpart.c_str(), day(timenow), year(timenow));

if spart is sized in the right way for never overflowing, that will work. (the parameters are known and you know the max size of each component, so it's just adding the max sizes plus the spaces and other things you want in that string)

if you are unsure or could modify the formatting string in the future, you could use snprintf() or slprintf() to ensure you don't overflow.

PS: it's good to store the result of sprintf() in a variable, could be useful to check all went fine, but if you don't check it... :slight_smile:

PS': if you return fstring anyway (local variable - returning a copy will be what happens), why don't you build it in fstring directly ?

dpart.c_str() interesting because I used that before the current String solution. dpart and mpart where then declared as char *. The result was 2 strings with nonsense, a string conversion of the pointer...
Same with strcpy().

That's the reason I ultimately was drawn to String. Apparently String class/constructor does the copying and errorchecking, see Wstring.h and Wstring.cpp: (lots of code for just copying a string to another mem location...). Extract below.

Wstring.h

    public:
        // constructors
        // creates a copy of the initial value.
        // if the initial value is null or invalid, or if memory allocation
        // fails, the string will be marked as invalid (i.e. "if (s)" will
        // be false).
        String(const char *cstr = "");

WString.cpp

String::String(const char *cstr) {
    init();
    if (cstr)
        copy(cstr, strlen(cstr));
}

And

String & String::copy(const char *cstr, unsigned int length) {
    if(!reserve(length)) {
        invalidate();
        return *this;
    }
    memmove(wbuffer(), cstr, length + 1);
    setLen(length);
    return *this;
}

The one-string formatting argument of sprintf, mainly.

Like this?

      if (n < 1) {
          Serial.printf("Date string is %d characters long: %s \n", n, spart);
          fstring = std::string(spart);
      }
      else {
          fstring = "==date formatting error==";
      }

I meant just use std::string (and std::ostringstream because I don't think the ESP32 has toString() implemented). I don't know why you would use String on top and the cString functions like sprintf()....

#include <TimeLib.h>  //  https://github.com/PaulStoffregen/Time
#include <sstream>

std::string FormatTimeString(time_t timenow) {
  std::string fstring = "";
  std::ostringstream d, y;
  d << day(timenow);
  y << year(timenow);
  fstring = dayStr(weekday(timenow));
  fstring += " ";
  fstring += monthStr(month(timenow));
  fstring += " ";
  fstring += d.str();
  fstring += " ";
  fstring += y.str();
  return fstring; // will send a copy
}

void setup() {
  Serial.begin(115200);
  time_t now = 1627490085ul;
  Serial.print("Date is: [");
  Serial.print(FormatTimeString(now).c_str());
  Serial.println("]");
}

void loop() {}

This worked too (although some weird intellisense " incomplete type is not allowed" error on sfstring):

std::string FormatTimeString(time_t timenow) {
  std::string fstring = "";
  std::ostringstream sfstring(std::ios_base::app); // append mode
  sfstring << dayStr(weekday(timenow)) << ", " << monthStr(month(timenow)) << " " << day(timenow) << " " << year(timenow);
  fstring = sfstring.str();
  return fstring; // will send a copy
}

sure, the trick is to use std::ostringstream to get the numbers converted into a string.

that being said, sprintf() does the job too :slight_smile: