Hi, I'm trying to implement a simple command buffer using Arduino's String class (from WString.h). The idea is that the command comes from SerialEvent() and is stored in the buffer, until it gets a special symbol considered as command terminator, after which the String gets to be processed elsewhere. The buffer itself is written as a member of my own class like so:
class MyClass
{
protected:
String m_buffer;
};
Once I'm done with the command, I send it to be parsed and reset the buffer by doing m_buffer = "".
However, I've found out that the memory used by this String seems to only grow with the length of the string that happens to be stored in m_buffer (unless its destructor is called).
Going through the sources, I've found out that it only reallocates if the string we're trying to save in it is bigger than whatever it had stored previously (see String's assignment operators etc for example). It seems to never shrink memorywise, if the new string is less than whatever buffer it had allocated previously (no realloc in those cases).
So I have a few questions:
Is my finding correct or am I missing something important?
If it is, is it by design or not?
If it is by design, then why there is no public function to clear() it properly and re-use the object?
Are the only two options for my case is to either allocate String object dynamically or deriving my own MyString class and implementing my own clear() member function in it?
String qwevdf ="";
int buffersize = 300;
voiding setup()
{
qwevdf.reserve( buffersize);
}
voiding thething ()
{
// add a thing to the String Buffer
qwevdf.concat("gobblygooks");//puts something in the String buffer
qwevdf = ""; //empties the String Buffer.
qwevdf = "sdvigoigoioiboivofef";//breaks the String buffer and may cause memory holes and other stuff that is bad.
}
Is there a known maximum size that you need? If so, just allocate (i.e. 'new') that much, use it, then de-allocate (i.e. 'delete') it when you're done. Or, just make an array that big as a class member and reuse it.
If I understand correctly, reserve() just does the same thing as any other assignment operator/constructor would do, i.e. it it reallocs only if whatever buffer was allocated previously is less than the number provided. It does not seem to set a hard (or any other) limit on the amount of bytes the string stores.
I also dont see how erasing the string with = "" will break it, since internally all it does is strcpy the new one in place of the old one (making sure the buffer is big enough prior ofcorse)
Thank you for noticing it, String::invalidate() does free the buffer and the object remains intact as long as you don't go digging into its const char* values before assigning the new one. I guess this might be why it is marked as protected. But then String::~String(void) should have been marked as virtual too (which it isn't).
I am hesitant to change "standard" arduino code, because it will certanly lead into some funky error messages whenever my co-workers (or me in a few weeks) will try to build my sketch on their end
So, instead I did this:
class StringCleanable : public String
{
public:
StringCleanable(const char *cstr = "") : String(cstr) {};
StringCleanable(const StringCleanable &str) : String(str) {};
StringCleanable(const __FlashStringHelper *str) : String(str) {};
// I do not use any of these constructors, but someone might, so...
explicit StringCleanable(char c) : String(c) {};
explicit StringCleanable(unsigned char c, unsigned char base=10) : String(c, base) {};
explicit StringCleanable(int n, unsigned char base=10) : String(n, base) {};
explicit StringCleanable(unsigned int n, unsigned char base=10) : String(n, base) {};
explicit StringCleanable(long n, unsigned char base=10) : String(n, base) {};
explicit StringCleanable(unsigned long n, unsigned char base=10) : String(n, base) {};
explicit StringCleanable(float n, unsigned char decimalPlaces=2) : String(n, decimalPlaces) {};
explicit StringCleanable(double n, unsigned char decimalPlaces=2) : String(n, decimalPlaces) {};
// String does not have a virtual destructor, but here it does not matter
~StringCleanable() = default;
// Reset always cleans the buffer, and reserves for new one if needed
unsigned char clean(size_t newReservedSize = 0) noexcept
{
unsigned char ret = 1;
invalidate();
if(newReservedSize)
{
ret = reserve(newReservedSize);
}
return ret;
}
// Allows for easy access to String's assignment operators etc,
// that are implicitly shadowed by StringCleanable
String& asString() noexcept
{
return *this;
}
};
With this, I can use this string naturally as a String as well as clean(...) it at will. Also, cases where shadowed function/operator is needed are handled by calling asString() first.